UNPKG

12.7 kBJavaScriptView Raw
1/**
2 * Linear Algebra toolkit for manipulating vectors and matrices. Mainly works using plain
3 * JavaScript arrays.
4 */
5
6/**
7 * Find the shape of an array, i.e. the number of elements per dimension of the array
8 *
9 * @param Array[...[mixed]] A Array to find shape of
10 * @return Array[Number] shape Array specifying the number of elements per dimension. n-th element
11 * corresponds to the number of elements in the n-th dimension.
12 */
13function getShape(A) {
14 if (!Array.isArray(A)) {
15 return [];
16 }
17
18 const B = getShape(A[0]);
19 B.unshift(A.length);
20
21 return B;
22}
23
24/**
25 * Generate n points on the interval (a,b), with intervals (b-a)/(n-1)
26 *
27 * @param Number a Starting point
28 * @param Number a Ending point
29 * @param Integer n Number of points
30 */
31function linspace(a, b, n) {
32 const r = [];
33
34 for (let i = 0; i < n; i += 1) {
35 r.push(a + i * ((b - a) / (n - 1)));
36 }
37
38 return r;
39}
40
41/**
42 * Initialize a vector of a certain length with a specific value in each entry
43 *
44 * @param Integer n Number of elements in the vector
45 * @param mixed value Value to initialize entries at
46 * @return Array Vector of n elements of the specified value
47 */
48function valueVector(n, value) {
49 return [...Array(n)].map(() => value);
50}
51
52/**
53 * Initialize an n-dimensional array of a certain value
54 *
55 * @param Array[Number] shape Array specifying the number of elements per dimension. n-th element
56 * corresponds to the number of elements in the n-th dimension.
57 * @param mixed value Value to fill the array with
58 * @return Array[...[mixed]] Array of the specified with zero in all entries
59 */
60function full(shape, value) {
61 if (shape.length === 1) {
62 return valueVector(shape[0], value);
63 }
64
65 return [...Array(shape[0])].map(() => full(shape.slice(1), value));
66}
67
68/**
69 * Initialize a zero vector of a certain length
70 *
71 * @param Integer n Number of elements in the vector
72 * @return Array Vector of n elements of value 0
73 */
74function zeroVector(n) {
75 return valueVector(n, 0);
76}
77
78/**
79 * Initialize an n-dimensional array of zeros
80 *
81 * @param Array[Number] shape Array specifying the number of elements per dimension. n-th element
82 * corresponds to the number of elements in the n-th dimension.
83 * @return Array[...[mixed]] Array of the specified with zero in all entries
84 */
85function zeros(shape) {
86 return full(shape, 0);
87}
88
89/**
90 * Set all entries in an array to a specific value
91 *
92 * @param Array[...[mixed]] A Array of which entries should be changed
93 * @param mixed value Value the array entries should be changed to
94 * @return Array[...[mixed]] Array with modified entries
95 */
96function fill(A, value) {
97 return A.map(B => (Array.isArray(B) ? fill(B, value) : value));
98}
99
100/**
101 * Concatenate two or more n-dimensional arrays.
102 *
103 * @param Integer axis Axis to perform concatenation on
104 * @param Array[...[mixed]] ...S Arrays to concatenate. They must have the same shape, except
105 * in the dimension corresponding to axis (the first, by default)
106 * @return Array Concatenated array
107 */
108function concatenate(axis, ...S) {
109 if (axis === 0) {
110 return [].concat(...S);
111 }
112
113 const A = [];
114
115 for (let i = 0; i < S[0].length; i += 1) {
116 A.push(concatenate(axis - 1, ...S.map(APrime => APrime[i])));
117 }
118
119 return A;
120}
121
122/**
123 * Repeat an array multiple times along an axis. This is essentially one or more concatenations of
124 * an array with itself
125 *
126 * @param Integer axis Axis to perform repetition on
127 * @param Integer numRepeats Number of times to repeat the array
128 * @param Array[...[mixed]] A Array to repeat
129 * @return Array[...[mixed]] Specified array repeated numRepeats times
130 */
131function repeat(axis, numRepeats, A) {
132 let R = A.slice();
133
134 for (let i = 0; i < numRepeats - 1; i += 1) {
135 R = concatenate(axis, R, A);
136 }
137
138 return R;
139}
140
141/**
142 * Pad an array along one or multiple axes
143 *
144 * @param Array[...[mixed]] A Array to be padded
145 */
146function pad(A, paddingLengths = [[1, 1]], paddingValues = [[0, 0]], axes = []) {
147 let B = A.slice();
148
149 // Use default axes to padded (first n axes where n is the number of axes used in paddingLenghts
150 // and paddingValues)
151 if (!axes.length) {
152 for (let i = 0; i < paddingLengths.length; i += 1) {
153 axes.push(i);
154 }
155 }
156
157 // Pad all specified axes
158 for (let i = 0; i < axes.length; i += 1) {
159 const axis = axes[i];
160 const currentShape = getShape(B);
161
162 // Determine padding lengths
163 let lengthFront = 0;
164 let lengthBack = 0;
165
166 if (Array.isArray(paddingLengths[i])) {
167 lengthFront = paddingLengths[i][0];
168 lengthBack = paddingLengths[i][1];
169 } else {
170 lengthFront = paddingLengths[i];
171 lengthBack = paddingLengths[i];
172 }
173
174 // Determine padding values
175 let valueFront = 0;
176 let valueBack = 0;
177
178 if (Array.isArray(paddingValues[i])) {
179 valueFront = paddingValues[i][0];
180 valueBack = paddingValues[i][1];
181 } else {
182 valueFront = paddingValues[i];
183 valueBack = paddingValues[i];
184 }
185
186 // Shape of padding for front and back
187 const shapeFront = currentShape.slice();
188 const shapeBack = currentShape.slice();
189
190 shapeFront[axis] = lengthFront;
191 shapeBack[axis] = lengthBack;
192
193 // Create padding blocks
194 const paddingFront = full(shapeFront, valueFront);
195 const paddingBack = full(shapeBack, valueBack);
196
197 B = concatenate(axis, paddingFront, B, paddingBack);
198 }
199
200 return B;
201}
202
203/**
204 * Calculate dot product of two vectors. Vectors should have same size.
205 *
206 * @param Array[Number] x First vector
207 * @param Array[Number] y Second vector
208 * @return Number Dot product scalar result
209 */
210function dot(x, y) {
211 return x.reduce((r, a, i) => r + a * y[i], 0);
212}
213
214/**
215 * Calculate the Euclidian norm of a vector
216 *
217 * @param Array[Number] x Vector of which to calculate the norm
218 */
219function norm(x) {
220 return Math.sqrt(dot(x, x));
221}
222
223/**
224 * Calculate sum of two vectors. Vectors should have same size.
225 *
226 * @param Array[Number] x First vector
227 * @param Array[Number] y Second vector
228 * @return Array[Number] Sum of vectors
229 */
230function sum(x, y) {
231 return x.map((a, i) => a + y[i]);
232}
233
234/**
235 * Multiply a vector by a scalar (i.e. scale the vector)
236 *
237 * @param Array[Number] x Vector
238 * @param Number c Scalar
239 * @return Array[Number] Scaled vector
240 */
241function scale(x, c) {
242 return x.map(a => c * a);
243}
244
245/**
246 * Sum all elements of an array
247 *
248 * @param Array[...[Number]] x Array
249 * @return Number Sum of all vector elements
250 */
251function internalSum(A) {
252 return A.reduce((r, B) => r + (Array.isArray(B) ? internalSum(B) : B), 0);
253}
254
255/**
256 * Get a copy of an array with absolute values of the original array entries
257 *
258 * @param Array[...[Number]] A Array to get absolute values array from
259 * @return Array[...[Number]] Array with absolute values
260 */
261function abs(A) {
262 return A.map(B => (Array.isArray(B) ? abs(B) : Math.abs(B)));
263}
264
265/**
266 * Randomly permute the rows of a matrix
267 *
268 * @param Array[Array[mixed]] S Matrix
269 * @param Array[Array[mixed]] ... Other matrices to permute in the same way
270 * @param Array[Array]
271 * @return Array[Array[mixed]] Permuted matrix
272 */
273function permuteRows(...S) {
274 // Copy matrices
275 const SPermutated = S.map(A => A.slice());
276
277 // Number of remaining rows
278 let remainingRows = SPermutated[0].length;
279
280 while (remainingRows > 0) {
281 // Select a random element from the remaining rows and swap it with the first element that has
282 // not yet been assigned
283 const swapIndex = Math.floor(Math.random() * remainingRows);
284
285 for (let i = 0; i < SPermutated.length; i += 1) {
286 const tmpRow = SPermutated[i][remainingRows - 1];
287
288 SPermutated[i][remainingRows - 1] = SPermutated[i][swapIndex];
289 SPermutated[i][swapIndex] = tmpRow;
290 }
291
292 remainingRows -= 1;
293 }
294
295 return SPermutated;
296}
297
298/**
299 * Recursively flatten an array
300 *
301 * @param Array[...[mixed]] A Array to be flattened
302 * @return Array[mixed] Flattened array
303 */
304function flatten(A) {
305 return [].concat(...A.map(x => (Array.isArray(x) ? flatten(x) : x)));
306}
307
308/**
309 * Get the transpose of a matrix or vector
310 *
311 * @param Array[Array[Number]] A Matrix or vector
312 * @return Array[Array[Number]] Transpose of the matrix
313 */
314function transpose(A) {
315 const ATranspose = zeros([A[0].length, A.length]);
316
317 for (let i = 0; i < A.length; i += 1) {
318 for (let j = 0; j < A[0].length; j += 1) {
319 ATranspose[j][i] = A[i][j];
320 }
321 }
322
323 return ATranspose;
324}
325
326/**
327 * Generate a mesh grid, i.e. two m-by-n arrays where m=|y| and n=|x|, from two vectors. The mesh
328 * grid generates two grids, where the first grid repeats x row-wise m times, and the second grid
329 * repeats y column-wise n times. Can be used to generate coordinate grids
330 *
331 * Example input: x=[0, 1, 2], y=[2, 4, 6, 8]
332 * Corresponding output:
333 * matrix 1: [[0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]]
334 * matrix 2: [[2, 2, 2], [4, 4, 4], [6, 6, 6], [8, 8, 8]]
335 *
336 * @param Array[Number] x Vector of x-coordinates
337 * @param Array[Number] y Vector of y-coordinates
338 * @return [Array[Array[Number]], Array[Array[Number]]] Grids
339 */
340function meshGrid(x, y) {
341 const gridX = transpose(repeat(1, y.length, x));
342 const gridY = repeat(1, x.length, y);
343
344 return [gridX, gridY];
345}
346
347/**
348 * Set an arbitrary element in an array, using another array to determine the index inside the array
349 *
350 * @param Array[...[mixed]] A Array to set an element in
351 * @param Array[Number] index Indices to find array element. n-th element corresponds to index in
352 * n-th dimension
353 * @param mixed value New element value at index
354 */
355function setArrayElement(A, index, value) {
356 const B = A.slice();
357
358 B[index[0]] = index.length === 1 ? value : setArrayElement(A[index[0]], index.slice(1), value);
359 return B;
360}
361
362/**
363 * Reshape an array into a different shape
364 *
365 * @param Array[...[mixed]] A Array to reshape
366 * @param Array[Number] shape Array specifying the number of elements per dimension. n-th element
367 * corresponds to the number of elements in the n-th dimension.
368 * @return Array[...[mixed]] Reshaped array
369 */
370function reshape(A, shape) {
371 const AValues = flatten(A);
372
373 let B = zeros(shape);
374
375 const counters = zeroVector(shape.length);
376 let counterIndex = counters.length - 1;
377
378 let counterTotal = 0;
379
380 let done = false;
381
382 while (!done) {
383 B = setArrayElement(B, counters, AValues[counterTotal]);
384
385 // Increment current counter
386 counterIndex = counters.length - 1;
387 counters[counterIndex] += 1;
388 counterTotal += 1;
389
390 // If the end of the current counter is reached, move to the next counter...
391 while (counters[counterIndex] === shape[counterIndex]) {
392 // ...unless we have reached the end of all counters. In that case, we are done
393 if (counterIndex === 0) {
394 done = true;
395 }
396
397 counters[counterIndex - 1] += 1;
398 counters[counterIndex] = 0;
399 counterIndex -= 1;
400 }
401 }
402
403 return B;
404}
405
406/**
407 * Extract a sub-block of a matrix of a particular shape at a particular position
408 *
409 * @param Array[...[mixed]] A Array to extract block from
410 * @param Array[Number] offset Array specifying the offset per dimension. n-th element corresponds
411 * to the number of elements to skip, before extracting the block, in the n-th
412 * dimension.
413 * @param Array[Number] shape Array specifying the number of elements per dimension. n-th element
414 * corresponds to the number of elements in the n-th dimension.
415 * @return Array[...[mixed]] Sub-block extracted from array
416 */
417function subBlock(A, offset, shape) {
418 if (offset.length === 1) {
419 return A.slice(offset[0], offset[0] + shape[0]);
420 }
421
422 const subblock = [];
423
424 for (let i = offset[0]; i < offset[0] + shape[0]; i += 1) {
425 subblock.push(subBlock(A[i], offset.slice(1), shape.slice(1)));
426 }
427
428 return subblock;
429}
430
431/**
432 * Get an arbitrary element from an array, using another array to determine the index inside the
433 * first array
434 *
435 * @param Array[...[mixed]] A Array to get an element from
436 * @param Array[Number] index Indices to find array element. n-th element corresponds to index in
437 * n-th dimension
438 * @return mixed Array element value at index
439 */
440function getArrayElement(A, index) {
441 if (index.length === 1) {
442 return A[index];
443 }
444
445 return getArrayElement(A[index[0]], index.slice(1));
446}
447
448export default {
449 linspace,
450 zeroVector,
451 valueVector,
452 zeros,
453 full,
454 fill,
455 pad,
456 getShape,
457 dot,
458 norm,
459 sum,
460 scale,
461 internalSum,
462 abs,
463 permuteRows,
464 flatten,
465 concatenate,
466 transpose,
467 meshGrid,
468 repeat,
469 reshape,
470 subBlock,
471 setArrayElement,
472 getArrayElement,
473};