array-kit 0.2.11 → 0.2.13

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.
Files changed (2) hide show
  1. package/lib/NDArray.js +207 -65
  2. package/package.json +1 -1
package/lib/NDArray.js CHANGED
@@ -42,10 +42,10 @@
42
42
  params: an object of optional parameters, where:
43
43
  order: array of coord order stored in the storage, by default [ 0 , 1 , 2 , ..., N ], for 2D it's row-first,
44
44
  if you want column-first, use [ 1 , 0 ], or params.reverse = true
45
- reverse: syntactic sugar, like params.order = [ N , ... , 2 , 1 , 0 ]
45
+ reverse: syntactic sugar, like params.order = [ N , ... , 2 , 1 , 0 ], means that the slowest moving dimension comes first
46
46
  dataStart: the index in the data storage where the ND-array starts (e.g. the start of the view in the Buffer)
47
- stride: (default: 1) the stride of the fastest moving dimension, usually 1 except if there is more dimensions fastest
48
- dimensions that are removed from the view
47
+ stride: (default: 1) the stride of the fastest moving dimension, usually 1 except if there is faster dimensions
48
+ that are removed from the view
49
49
 
50
50
  Not added yet:
51
51
  offset: the offset of the first logical element of the ND-array
@@ -86,14 +86,21 @@ function NDArray( dataOrConstructor , sizes , params ) {
86
86
  // Define the layout of the array in memory, strides are products of all faster dimensions
87
87
  this.strides = new Array( dimensions ) ;
88
88
 
89
+ // Use to speed up the whole array iteration
90
+ this.backStrides = new Array( dimensions ) ;
91
+
89
92
  // This is the index of the first logical element in the physical data storage
90
93
  this.offset = 0 ;
91
94
 
95
+ // True if the physical backend is contiguous, that can speed up some operation like .fill()
96
+ this.isContiguous = true ;
97
+
92
98
  // This is physical start and end of the ND-array in the data storage
93
99
  this.dataStart = 0 ;
94
100
  this.dataEnd = 0 ;
95
101
 
96
- // The underlying data store: Array, TypedArray, Buffer... anything that is indexed
102
+ // The underlying physical data store backend.
103
+ // Can be: Array, TypedArray, Buffer... anything that is indexed.
97
104
  this.data = null ;
98
105
 
99
106
  if ( ! noInit ) {
@@ -128,7 +135,7 @@ NDArray.prototype._init = function( dataOrConstructor , sizes , params = {} ) {
128
135
  }
129
136
 
130
137
  // Compute the stride
131
- this._initStridesUsingSizes( params.stride ) ;
138
+ this._initStridesFromSizes( params.stride ) ;
132
139
 
133
140
  // The index where the ND-array region starts
134
141
  this.dataStart = params.dataStart || 0 ;
@@ -140,7 +147,7 @@ NDArray.prototype._init = function( dataOrConstructor , sizes , params = {} ) {
140
147
  // Set or create a new data store
141
148
  if ( typeof dataOrConstructor === 'function' ) {
142
149
  if ( typeof dataOrConstructor.allocUnsafe === 'function' ) {
143
- // Detect Node.js's Buffer referencing it (avoid browser compatibility layer)
150
+ // Detect Node.js's Buffer without referencing it (avoid browser compatibility layer)
144
151
  this.data = dataOrConstructor.allocUnsafe( this.dataEnd ) ;
145
152
  }
146
153
  else {
@@ -161,6 +168,8 @@ NDArray.prototype._init = function( dataOrConstructor , sizes , params = {} ) {
161
168
  // Fragmented init, because it can be re-use by various cloning things
162
169
 
163
170
  NDArray.prototype._initMinsMaxsSizesFromRegion = function( region ) {
171
+ this.size = 1 ;
172
+
164
173
  for ( let d = 0 ; d < this.dimensions ; d ++ ) {
165
174
  this.mins[ d ] = region[ d ][ 0 ] ;
166
175
  this.maxs[ d ] = region[ d ][ 1 ] ;
@@ -200,13 +209,23 @@ NDArray.prototype._initMinsMaxsFromSizes = function( sizes ) {
200
209
  } ;
201
210
 
202
211
  // Sizes is not passed as argument, it should already be computed on the instance
203
- NDArray.prototype._initStridesUsingSizes = function( stride = 1 ) {
212
+ NDArray.prototype._initStridesFromSizes = function( nextStride = 1 ) {
213
+ // It's always contiguous when strides start at 1
214
+ this.isContiguous = nextStride === 1 ;
215
+
204
216
  for ( const d of this.order ) {
205
- this.strides[ d ] = stride ;
206
- stride *= this.sizes[ d ] ;
217
+ this.strides[ d ] = nextStride ;
218
+ this.backStrides[ d ] = ( 1 - this.sizes[ d ] ) * nextStride ;
219
+ nextStride *= this.sizes[ d ] ;
207
220
  }
208
221
  } ;
209
222
 
223
+ // Strides is not passed as argument, it should already be computed on the instance
224
+ NDArray.prototype._initOrderFromStrides = function() {
225
+ this.order = Array.from( { length: this.strides.length } , ( v , i ) => i ) ;
226
+ this.order.sort( ( a , b ) => this.strides[ a ] - this.strides[ b ] ) ;
227
+ } ;
228
+
210
229
  // Offset, sizes and strides are not passed as argument, they should already be computed on the instance
211
230
  NDArray.prototype._initOffsetEndFromStartSizesStrides = function() {
212
231
  this.dataEnd = this.dataStart + 1 ;
@@ -225,7 +244,6 @@ NDArray.prototype._initOffsetEndFromStartSizesStrides = function() {
225
244
  }
226
245
  } ;
227
246
 
228
- /* Not sure if it's useful to compute based on offset
229
247
  // Offset, sizes and strides are not passed as argument, they should already be computed on the instance
230
248
  NDArray.prototype._initStartEndFromOffsetSizesStrides = function() {
231
249
  this.dataStart = this.offset ;
@@ -240,7 +258,62 @@ NDArray.prototype._initStartEndFromOffsetSizesStrides = function() {
240
258
  }
241
259
  }
242
260
  } ;
243
- */
261
+
262
+ // Init .isContiguous based on strides and sizes.
263
+ // Usually only called for views, because if ._initStridesFromSizes() is called it already set it to the right value.
264
+ NDArray.prototype._initIsContiguousFromStridesSizes = function() {
265
+ let nextStride = 1 ;
266
+
267
+ for ( const d of this.order ) {
268
+ const absStride = Math.abs( this.strides[ d ] ) ;
269
+
270
+ if ( absStride !== nextStride ) {
271
+ this.isContiguous = false ;
272
+ return ;
273
+ }
274
+
275
+ nextStride *= absStride ;
276
+ }
277
+
278
+ this.isContiguous = true ;
279
+ } ;
280
+
281
+
282
+
283
+ // Create a new data back-end from another one
284
+ // sliceStart and sliceEnd are optional
285
+ NDArray.prototype._createDataFrom = function( data , sliceStart = 0 , sliceEnd = data.length ) {
286
+ if ( Array.isArray( data ) ) {
287
+ this.data = data.slice( sliceStart , sliceEnd ) ;
288
+ }
289
+ else if ( typeof data.constructor.allocUnsafe === 'function' ) {
290
+ // Detect Node.js's Buffer without referencing it (avoid browser compatibility layer)
291
+ this.data = data.constructor.allocUnsafe( sliceEnd - sliceStart ) ;
292
+ data.copy( this.data , 0 , sliceStart , sliceEnd ) ;
293
+ }
294
+ else if ( typeof data.slice === 'function' ) {
295
+ this.data = data.slice( sliceStart , sliceEnd ) ;
296
+ }
297
+ else {
298
+ this.data = new data.constructor( data.length ) ;
299
+
300
+ for ( let src = sliceStart , dst = 0 ; src < sliceEnd ; src ++ , dst ++ ) {
301
+ this.data[ dst ] = data[ src ] ;
302
+ }
303
+ }
304
+ } ;
305
+
306
+
307
+
308
+ // Create a new data store of the same type
309
+ NDArray.prototype._newStorageOfTheSameKind = function( size = this.dataEnd - this.dataStart ) {
310
+ if ( typeof this.data?.constructor?.allocUnsafe === 'function' ) {
311
+ // Detect Node.js's Buffer referencing it (avoid browser compatibility layer)
312
+ return this.data.constructor.allocUnsafe( size ) ;
313
+ }
314
+
315
+ return new this.data.constructor( size ) ;
316
+ } ;
244
317
 
245
318
 
246
319
 
@@ -283,6 +356,7 @@ NDArray.prototype.flip = function( dimension ) {
283
356
  }
284
357
 
285
358
  this.strides[ dimension ] *= - 1 ;
359
+ this.backStrides[ dimension ] *= - 1 ;
286
360
  this._initOffsetEndFromStartSizesStrides() ;
287
361
 
288
362
  return this ;
@@ -290,16 +364,78 @@ NDArray.prototype.flip = function( dimension ) {
290
364
 
291
365
  // Flip coordinates along all dimensions at once, but preserving min and max
292
366
  NDArray.prototype.flipAll = function() {
293
- for ( let d = 0 ; d < this.dimensions ; d ++ ) { this.strides[ d ] *= - 1 ; }
367
+ for ( let d = 0 ; d < this.dimensions ; d ++ ) {
368
+ this.strides[ d ] *= - 1 ;
369
+ this.backStrides[ d ] *= - 1 ;
370
+ }
371
+
294
372
  this._initOffsetEndFromStartSizesStrides() ;
295
373
  return this ;
296
374
  } ;
297
375
 
298
376
 
299
377
 
378
+ /*
379
+ Fix some axis and reduce the number of dimensions.
380
+ Like .pick() in the "ndarray" package.
381
+
382
+ Examples:
383
+ .select( null , 3 , null ) fix the second coordinate to 3, the ND-array will now have 2 dimensions instead of 3
384
+ .select( 1 , 3 , null ) fix the first and second coordinates respectively to 1 and 3, the ND-array will now have 1 dimensions instead of 3
385
+ */
386
+ NDArray.prototype.select = function( ... coords ) {
387
+ this.size = 1 ;
388
+
389
+ // dimensions should be cached because it's modified along the way
390
+ for ( let d = 0 , newD = 0 , dimensions = this.dimensions ; d < dimensions ; d ++ ) {
391
+ const coord = coords[ d ] ;
392
+
393
+ if ( coord === null || coord === undefined ) {
394
+ this.size *= this.sizes[ newD ] ;
395
+ newD ++ ;
396
+ }
397
+ else {
398
+ this.offset += ( coord - this.mins[ newD ] ) * this.strides[ newD ] ;
399
+ this.sizes.splice( newD , 1 ) ;
400
+ this.mins.splice( newD , 1 ) ;
401
+ this.maxs.splice( newD , 1 ) ;
402
+ this.strides.splice( newD , 1 ) ;
403
+ this.backStrides.splice( newD , 1 ) ;
404
+ this.dimensions -- ;
405
+ }
406
+ }
407
+
408
+ this._initOrderFromStrides() ;
409
+ this._initStartEndFromOffsetSizesStrides() ;
410
+ this._initIsContiguousFromStridesSizes() ;
411
+
412
+ return this ;
413
+ } ;
414
+
415
+
416
+
417
+ // Clone only the view, the data storage is the same
418
+ NDArray.prototype.cloneView =
419
+ NDArray.prototype.view = function() {
420
+ const newNDArray = this._cloneGeometry( true ) ;
421
+ newNDArray.data = this.data ;
422
+ return newNDArray ;
423
+ } ;
424
+
425
+
426
+
427
+ // Full clone
428
+ NDArray.prototype.clone = function() {
429
+ const newNDArray = this._cloneGeometry() ;
430
+ newNDArray._createDataFrom( this.data , this.dataStart , this.dataEnd ) ;
431
+ return newNDArray ;
432
+ } ;
433
+
434
+
435
+
300
436
  // Create a new clone of the current NDArray with the same geometry but an empty (but ready) data
301
- // The new data is limited to the used size (dataStart = 0).
302
- NDArray.prototype._cloneGeometry = function() {
437
+ // The new data is limited to the used size (dataStart = 0) except if keepDataStart is set to true.
438
+ NDArray.prototype._cloneGeometry = function( keepDataStart = false ) {
303
439
  const newNDArray = new NDArray( this.dimensions ) ;
304
440
 
305
441
  // Copy geometry
@@ -309,10 +445,18 @@ NDArray.prototype._cloneGeometry = function() {
309
445
  Object.assign( newNDArray.mins , this.mins ) ;
310
446
  Object.assign( newNDArray.maxs , this.maxs ) ;
311
447
  Object.assign( newNDArray.strides , this.strides ) ;
448
+ Object.assign( newNDArray.backStrides , this.backStrides ) ;
312
449
 
313
- // Start at index=0 in the physical storage
314
- newNDArray.offset = this.offset - this.dataStart ;
315
- newNDArray.dataEnd = this.dataEnd - this.dataStart ;
450
+ if ( keepDataStart ) {
451
+ newNDArray.dataStart = this.dataStart ;
452
+ newNDArray.dataEnd = this.dataEnd ;
453
+ newNDArray.offset = this.offset ;
454
+ }
455
+ else {
456
+ // Start at index=0 in the physical storage
457
+ newNDArray.offset = this.offset - this.dataStart ;
458
+ newNDArray.dataEnd = this.dataEnd - this.dataStart ;
459
+ }
316
460
 
317
461
  return newNDArray ;
318
462
  } ;
@@ -325,7 +469,7 @@ NDArray.prototype._resizeGeometry = function( mins , maxs ) {
325
469
 
326
470
  Object.assign( newNDArray.order , this.order ) ;
327
471
  newNDArray._initSizesFromMinsMaxs( mins , maxs ) ;
328
- newNDArray._initStridesUsingSizes() ;
472
+ newNDArray._initStridesFromSizes() ;
329
473
  newNDArray._initOffsetEndFromStartSizesStrides() ;
330
474
 
331
475
  return newNDArray ;
@@ -333,24 +477,11 @@ NDArray.prototype._resizeGeometry = function( mins , maxs ) {
333
477
 
334
478
 
335
479
 
336
- // Create a new data store of the same type
337
- NDArray.prototype._newStorageOfTheSameKind = function( size = this.dataEnd - this.dataStart ) {
338
- if ( typeof this.data?.constructor?.allocUnsafe === 'function' ) {
339
- // Detect Node.js's Buffer referencing it (avoid browser compatibility layer)
340
- return this.data.constructor.allocUnsafe( size ) ;
341
- }
342
-
343
- return new this.data.constructor( size ) ;
344
- } ;
345
-
346
-
347
-
348
480
  // Internal version without any unnessessary test
349
481
  // Classic strided array access is:
350
482
  // index = coords[ 0 ] * strides[ 0 ] + coords[ 1 ] * strides[ 1 ] + ... + coords[ n ] * strides[ n ]
351
483
  NDArray.prototype._getIndex = function( coords ) {
352
484
  let index = this.offset ;
353
- //for ( const d of this.order ) { index += ( coords[ d ] - this.mins[ d ] ) * this.strides[ d ] ; }
354
485
  for ( let d = 0 ; d < this.dimensions ; d ++ ) { index += ( coords[ d ] - this.mins[ d ] ) * this.strides[ d ] ; }
355
486
  return index ;
356
487
  } ;
@@ -358,7 +489,6 @@ NDArray.prototype._getIndex = function( coords ) {
358
489
  NDArray.prototype._getIndexCheck = function( coords ) {
359
490
  let index = this.offset ;
360
491
 
361
- //for ( const d of this.order ) {
362
492
  for ( let d = 0 ; d < this.dimensions ; d ++ ) {
363
493
  const c = coords[ d ] ;
364
494
 
@@ -537,31 +667,7 @@ NDArray.prototype._getStrideStartAndVectorDimension = function( coords ) {
537
667
  */
538
668
  NDArray.prototype.forEach =
539
669
  NDArray.prototype._forEach = function( callback ) {
540
- // Usually =1, but later we could group neighbours (e.g. for RGBA images => stride0 = 4)
541
- const stride0 = this.strides[ this.order[ 0 ] ] ;
542
- const coords = Array.from( this.mins ) ;
543
- let index = this.dataStart ;
544
-
545
- for ( ;; ) {
546
- callback( this.data[ index ] , coords , index , this ) ;
547
-
548
- index += stride0 ;
549
- if ( index >= this.dataEnd ) { return ; }
550
-
551
- for ( let i = 0 ; i < this.dimensions ; i ++ ) {
552
- const d = this.order[ i ] ;
553
- coords[ d ] ++ ;
554
- if (
555
- i < this.dimensions - 1
556
- && coords[ d ] - this.mins[ d ] >= this.strides[ this.order[ i + 1 ] ]
557
- ) {
558
- coords[ d ] = this.mins[ d ] ;
559
- }
560
- else {
561
- break ;
562
- }
563
- }
564
- }
670
+ return this._forEachInRegion( this.mins , this.maxs , callback , true ) ;
565
671
  } ;
566
672
 
567
673
 
@@ -597,10 +703,19 @@ NDArray.prototype._forEachInRegionCheck = function( mins , maxs , callback ) {
597
703
  this._forEachInRegion( mins , maxs , callback ) ;
598
704
  } ;
599
705
 
600
- NDArray.prototype._forEachInRegion = function( mins , maxs , callback ) {
601
- const backStrides = mins.map( ( min , d ) => ( min - maxs[ d ] ) * this.strides[ d ] ) ;
706
+ // wholeArray=true when coming from ._forEach()
707
+ NDArray.prototype._forEachInRegion = function( mins , maxs , callback , wholeArray = false ) {
708
+ let backStrides , index ;
602
709
  const coords = Array.from( mins ) ;
603
- let index = this._getIndex( coords ) ;
710
+
711
+ if ( wholeArray ) {
712
+ backStrides = this.backStrides ;
713
+ index = this.offset ;
714
+ }
715
+ else {
716
+ backStrides = mins.map( ( min , d ) => ( min - maxs[ d ] ) * this.strides[ d ] ) ;
717
+ index = this._getIndex( coords ) ;
718
+ }
604
719
 
605
720
  for ( ;; ) {
606
721
  callback( this.data[ index ] , coords , index , this ) ;
@@ -677,6 +792,13 @@ NDArray.prototype._forEachVectorInRegion = function( strideStartCoords , maxs ,
677
792
 
678
793
 
679
794
 
795
+ // Return a Generator
796
+ // .each()
797
+ NDArray.prototype.each =
798
+ NDArray.prototype._each = function() {
799
+ return this._eachInRegion( this.mins , this.maxs , true ) ;
800
+ } ;
801
+
680
802
  // Return a Generator
681
803
  // .eachInRegion( region )
682
804
  // .eachInRegion( mins , maxs )
@@ -696,10 +818,20 @@ NDArray.prototype._eachInRegionCheck = function( mins , maxs ) {
696
818
 
697
819
  // Generator
698
820
  // The yielded entry { coords , index , value } should be cloned, as well as entry.coords, if userland want to modify it
699
- NDArray.prototype._eachInRegion = function*( mins , maxs ) {
700
- const backStrides = mins.map( ( min , d ) => ( min - maxs[ d ] ) * this.strides[ d ] ) ;
821
+ // wholeArray=true when coming from ._each()
822
+ NDArray.prototype._eachInRegion = function*( mins , maxs , wholeArray = false ) {
823
+ let backStrides , index ;
701
824
  const coords = Array.from( mins ) ;
702
- let index = this._getIndex( coords ) ;
825
+
826
+ if ( wholeArray ) {
827
+ backStrides = this.backStrides ;
828
+ index = this.offset ;
829
+ }
830
+ else {
831
+ backStrides = mins.map( ( min , d ) => ( min - maxs[ d ] ) * this.strides[ d ] ) ;
832
+ index = this._getIndex( coords ) ;
833
+ }
834
+
703
835
  const entry = { coords , index , value: undefined } ;
704
836
 
705
837
  for ( ;; ) {
@@ -931,8 +1063,18 @@ NDArray.prototype._eachVectorIndexInRegionBackward = function*( strideStartCoord
931
1063
 
932
1064
 
933
1065
  NDArray.prototype.fill = function( value ) {
934
- const stride0 = this.strides[ this.order[ 0 ] ] ;
935
- for ( let i = this.dataStart ; i < this.dataEnd ; i += stride0 ) { this.data[ i ] = value ; }
1066
+ if ( this.isContiguous ) {
1067
+ // Fast path: iterate directly on the physical data
1068
+ for ( let i = this.dataStart ; i < this.dataEnd ; i ++ ) {
1069
+ this.data[ i ] = value ;
1070
+ }
1071
+ }
1072
+ else {
1073
+ for ( const { index } of this._each() ) {
1074
+ this.data[ index ] = value ;
1075
+ }
1076
+ }
1077
+
936
1078
  return this ;
937
1079
  } ;
938
1080
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "array-kit",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "An array manipulation toolbox.",
5
5
  "main": "lib/array-kit.js",
6
6
  "directories": {