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 | */
|
13 | function 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 | */
|
31 | function 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 | */
|
48 | function 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 | */
|
60 | function 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 | */
|
74 | function 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 | */
|
85 | function 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 | */
|
96 | function 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 | */
|
108 | function 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 | */
|
131 | function 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 | */
|
146 | function 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 | */
|
210 | function 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 | */
|
219 | function 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 | */
|
230 | function 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 | */
|
241 | function 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 | */
|
251 | function 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 | */
|
261 | function 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 | */
|
273 | function 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 | */
|
304 | function 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 | */
|
314 | function 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 | */
|
340 | function 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 | */
|
355 | function 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 | */
|
370 | function 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 | */
|
417 | function 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 | */
|
440 | function 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 |
|
448 | export 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 | };
|