carray 3.0.0 → 3.0.1

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +325 -3
  4. data/{NEWS.md → CHANGELOG.v1.md} +1 -1
  5. data/README.md +32 -32
  6. data/carray.gemspec +12 -8
  7. data/ext/ca_axis_group.c +55 -19
  8. data/ext/ca_binop_dispatch.c +5 -6
  9. data/ext/ca_binop_dispatch.h +0 -7
  10. data/ext/ca_categorical_iterator.c +13 -13
  11. data/ext/ca_for_each_element.h +6 -8
  12. data/ext/ca_group_iter.c +1 -1
  13. data/ext/ca_kernel_iterator.c +69 -23
  14. data/ext/ca_kernel_iterator.h +3 -10
  15. data/ext/ca_obj_bincmp.c +57 -27
  16. data/ext/ca_obj_binop.c +86 -30
  17. data/ext/ca_obj_const_string.c +3 -1
  18. data/ext/ca_obj_face.c +80 -0
  19. data/ext/ca_obj_face.h +33 -1
  20. data/ext/ca_obj_fake.c +11 -0
  21. data/ext/ca_obj_fixlen_string.c +3 -1
  22. data/ext/ca_obj_grid.c +11 -3
  23. data/ext/ca_obj_meld.c +13 -8
  24. data/ext/ca_obj_moncmp.c +33 -14
  25. data/ext/ca_obj_monop.c +19 -7
  26. data/ext/ca_obj_object.c +99 -4
  27. data/ext/ca_obj_record.c +3 -1
  28. data/ext/ca_obj_refer.c +30 -6
  29. data/ext/ca_obj_roll.c +13 -5
  30. data/ext/ca_obj_select_axis.c +16 -6
  31. data/ext/ca_obj_shift.c +3 -3
  32. data/ext/ca_obj_stride.c +106 -23
  33. data/ext/ca_obj_string.c +3 -1
  34. data/ext/ca_obj_tile.c +13 -5
  35. data/ext/ca_obj_time.c +3 -1
  36. data/ext/ca_obj_timedelta.c +3 -1
  37. data/ext/ca_obj_transpose.c +1 -1
  38. data/ext/ca_obj_triop.c +62 -23
  39. data/ext/ca_obj_window.c +86 -17
  40. data/ext/ca_op_cmplx64.h +123 -0
  41. data/ext/ca_op_ipower.c +0 -3
  42. data/ext/ca_sort_kernels.h +5 -5
  43. data/ext/ca_sweep_engine.c +78 -35
  44. data/ext/ca_sweep_engine.h +13 -4
  45. data/ext/ca_transform_common.c +7 -0
  46. data/ext/carray.h +127 -130
  47. data/ext/carray_access.c +56 -34
  48. data/ext/carray_bincount.c +8 -8
  49. data/ext/carray_broadcast.c +99 -6
  50. data/ext/carray_build_flags.h +3 -0
  51. data/ext/carray_call_cfunc.c +1491 -0
  52. data/ext/carray_call_cfunc.h +153 -0
  53. data/ext/carray_cast.c +69 -31
  54. data/ext/carray_conversion.c +22 -22
  55. data/ext/carray_copy.c +12 -2
  56. data/ext/carray_core.c +57 -10
  57. data/ext/carray_count.c +1 -1
  58. data/ext/carray_element.c +7 -7
  59. data/ext/carray_factorize.c +28 -28
  60. data/ext/carray_hold.c +1 -1
  61. data/ext/carray_index_classifier.c +7 -21
  62. data/ext/carray_internal.h +19 -3
  63. data/ext/carray_kernels_bincmp.c +1 -0
  64. data/ext/carray_kernels_binop.c +320 -298
  65. data/ext/carray_kernels_init.c +1095 -0
  66. data/ext/carray_kernels_map.c +1 -0
  67. data/ext/carray_kernels_moncmp.c +1 -0
  68. data/ext/carray_kernels_monop.c +412 -411
  69. data/ext/carray_kernels_reduce_aggregate.c +88 -87
  70. data/ext/carray_kernels_reduce_boolean.c +1 -0
  71. data/ext/carray_kernels_reduce_cumulative.c +54 -53
  72. data/ext/carray_kernels_reduce_extreme.c +51 -50
  73. data/ext/carray_kernels_reduce_variance.c +1 -0
  74. data/ext/carray_kernels_scan.c +1 -0
  75. data/ext/carray_kernels_search.c +93 -92
  76. data/ext/carray_kernels_sort.c +1 -0
  77. data/ext/carray_kernels_triop.c +1 -0
  78. data/ext/carray_lazy.c +175 -5
  79. data/ext/carray_mask.c +6 -5
  80. data/ext/carray_median_percentile.c +1 -1
  81. data/ext/carray_memory_view.c +46 -21
  82. data/ext/carray_operator.c +46 -49
  83. data/ext/carray_partition.c +4 -4
  84. data/ext/carray_random.c +7 -7
  85. data/ext/carray_scatter.c +1 -1
  86. data/ext/carray_sort.c +3 -3
  87. data/ext/carray_sort_kernel.c +10 -10
  88. data/ext/carray_test.c +1 -1
  89. data/ext/extconf.rb +18 -0
  90. data/ext/mk_call_cfunc.rb +243 -52
  91. data/ext/mkkernel.rb +354 -100
  92. data/ext/ruby_carray.c +18 -16
  93. data/ext/version.h +4 -4
  94. data/lib/carray/autoload_carray.rb +19 -0
  95. data/lib/carray/autoload_method_extension.rb +2 -1
  96. data/lib/carray/axis_group.rb +7 -7
  97. data/lib/carray/basics.rb +59 -59
  98. data/lib/carray/bincount_nd.rb +12 -12
  99. data/lib/carray/block_iterator.rb +24 -14
  100. data/lib/carray/categorical.rb +4 -4
  101. data/lib/carray/categorical_iterator.rb +98 -76
  102. data/lib/carray/conditional.rb +14 -14
  103. data/lib/carray/construct.rb +21 -0
  104. data/lib/carray/core_extensions.rb +62 -5
  105. data/lib/carray/data_type_extension.rb +18 -1
  106. data/lib/carray/frame/concat.rb +3 -3
  107. data/lib/carray/frame/convert.rb +1 -1
  108. data/lib/carray/frame/frame.rb +4 -11
  109. data/lib/carray/frame/io.rb +159 -2
  110. data/lib/carray/frame/sort.rb +1 -1
  111. data/lib/carray/frame/verbs.rb +18 -1
  112. data/lib/carray/fuse_source.rb +123 -0
  113. data/lib/carray/fusion.rb +218 -0
  114. data/lib/carray/histogram.rb +16 -16
  115. data/lib/carray/inspect.rb +1 -7
  116. data/lib/carray/iterator.rb +4 -3
  117. data/lib/carray/lazy.rb +125 -73
  118. data/lib/carray/meld_reduce.rb +2 -2
  119. data/lib/carray/methods/align_addr.rb +1 -1
  120. data/lib/carray/methods/composition.rb +1 -1
  121. data/lib/carray/methods/is_in.rb +12 -12
  122. data/lib/carray/methods/locate_addr.rb +6 -1
  123. data/lib/carray/methods/mask_duplicates.rb +1 -1
  124. data/lib/carray/methods/meshgrid.rb +4 -5
  125. data/lib/carray/methods/mode.rb +2 -2
  126. data/lib/carray/methods/nunique.rb +1 -1
  127. data/lib/carray/methods/snap.rb +7 -2
  128. data/lib/carray/methods/unique.rb +3 -3
  129. data/lib/carray/methods/value_counts.rb +2 -2
  130. data/lib/carray/runtime.rb +0 -19
  131. data/lib/carray/slab_iterator.rb +20 -7
  132. data/lib/carray/string_operation_extension.rb +5 -5
  133. data/lib/carray/time.rb +890 -491
  134. data/lib/carray/window_iterator.rb +285 -13
  135. data/lib/carray.rb +5 -5
  136. data/yard-stubs/ca_obj_array.rb +385 -0
  137. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  138. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  139. data/yard-stubs/ca_obj_block.rb +73 -0
  140. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  141. data/yard-stubs/ca_obj_fake.rb +31 -0
  142. data/yard-stubs/ca_obj_farray.rb +32 -0
  143. data/yard-stubs/ca_obj_field.rb +45 -0
  144. data/yard-stubs/ca_obj_grid.rb +35 -0
  145. data/yard-stubs/ca_obj_refer.rb +72 -0
  146. data/yard-stubs/ca_obj_roll.rb +45 -0
  147. data/yard-stubs/ca_obj_shift.rb +43 -0
  148. data/yard-stubs/ca_obj_stride.rb +181 -0
  149. data/yard-stubs/ca_obj_tile.rb +29 -0
  150. data/yard-stubs/ca_obj_transpose.rb +40 -0
  151. data/yard-stubs/ca_obj_window.rb +49 -0
  152. data/yard-stubs/carray_access.rb +131 -0
  153. data/yard-stubs/carray_attribute.rb +246 -0
  154. data/yard-stubs/carray_broadcast.rb +37 -0
  155. data/yard-stubs/carray_cast.rb +489 -0
  156. data/yard-stubs/carray_class.rb +65 -0
  157. data/yard-stubs/carray_conversion.rb +76 -0
  158. data/yard-stubs/carray_copy.rb +79 -0
  159. data/yard-stubs/carray_core.rb +114 -0
  160. data/yard-stubs/carray_count.rb +79 -0
  161. data/yard-stubs/carray_element.rb +108 -0
  162. data/yard-stubs/carray_generate.rb +66 -0
  163. data/yard-stubs/carray_lazy.rb +23 -0
  164. data/yard-stubs/carray_loop.rb +140 -0
  165. data/yard-stubs/carray_mask.rb +259 -0
  166. data/yard-stubs/carray_math.rb +132 -0
  167. data/yard-stubs/carray_mathfunc.rb +45 -0
  168. data/yard-stubs/carray_median_percentile.rb +89 -0
  169. data/yard-stubs/carray_memory_view.rb +163 -0
  170. data/yard-stubs/carray_order.rb +312 -0
  171. data/yard-stubs/carray_random.rb +89 -0
  172. data/yard-stubs/carray_scatter.rb +106 -0
  173. data/yard-stubs/carray_slab.rb +57 -0
  174. data/yard-stubs/carray_sort.rb +163 -0
  175. data/yard-stubs/carray_test.rb +85 -0
  176. data/yard-stubs/carray_undef.rb +64 -0
  177. data/yard-stubs/carray_utils.rb +97 -0
  178. data/yard-stubs/ruby_carray.rb +193 -0
  179. metadata +59 -10
  180. data/ext/ca_obj_unbound_repeat.c +0 -496
@@ -18,7 +18,7 @@
18
18
  # margins filled by the boundary policy), take its `sliding_windows` view
19
19
  # (a pure strided view over the padded buffer), and run a core reduction over
20
20
  # the trailing window axes. One vectorized pass; the named reductions
21
- # delegate straight to the core reduction, so their dtype / mask / empty
21
+ # delegate straight to the core reduction, so their data type / mask / empty
22
22
  # (ERI) / epsilon contracts are the core's, unchanged. This replaces the 2.0
23
23
  # per-anchor C engine (ext/ca_iter_window.c, retired).
24
24
  #
@@ -116,12 +116,21 @@ class CAWindowIterator < CAIterator
116
116
  # Trailing window axes of the sliding_windows view: [ndim .. 2*ndim-1].
117
117
  @window_axes = (@sndim...(2 * @sndim)).to_a
118
118
 
119
- # Output iteration space (reference-shaped, except :truncate which shrinks).
119
+ # Output iteration space (reference-shaped, except :truncate, which keeps
120
+ # only the anchors whose window lies wholly inside the source), and where
121
+ # each axis's first window starts in the buffer the windows are read from.
122
+ # A window that does not cover its own anchor -- `windows(1..2)`, the two
123
+ # cells after this one -- starts further along than the margin allows for,
124
+ # which is what @origins carries.
120
125
  rshape = @source.shape
121
126
  if @bounds == :truncate
122
- @shape = @sndim.times.map { |i| rshape[i] - @widths[i] + 1 }
127
+ first = @ranges.map { |r| [0, -r.begin].max }
128
+ last = @sndim.times.map { |i| [rshape[i] - 1, rshape[i] - 1 - @ranges[i].end].min }
129
+ @shape = @sndim.times.map { |i| [last[i] - first[i] + 1, 0].max }
130
+ @origins = @ranges.map { |r| [r.begin, 0].max }
123
131
  else
124
132
  @shape = rshape.dup
133
+ @origins = @sndim.times.map { |i| @lefts[i] + @ranges[i].begin }
125
134
  end
126
135
  @ndim = @shape.size
127
136
  self
@@ -149,11 +158,23 @@ class CAWindowIterator < CAIterator
149
158
  # the padded entity.
150
159
  # @return [CArray]
151
160
  def sliding_view
152
- @sliding_view ||= padded_entity.sliding_windows(*@widths)
161
+ @sliding_view ||= anchored_buffer.sliding_windows(*@widths)
153
162
  end
154
163
 
155
164
  private
156
165
 
166
+ # The stretch of the padded buffer the windows are taken from: it begins at
167
+ # the first window and holds one window per anchor. For a window that covers
168
+ # its own anchor and a boundary policy that pads, that is the whole buffer.
169
+ def anchored_buffer
170
+ buffer = padded_entity
171
+ wanted = @ndim.times.map { |k| @origins[k]...(@origins[k] + @shape[k] + @widths[k] - 1) }
172
+ return buffer if wanted.each_with_index.all? { |range, k|
173
+ range.begin.zero? && range.end == buffer.dim[k]
174
+ }
175
+ buffer[*wanted]
176
+ end
177
+
157
178
  # Build the padded entity (or, for :truncate, the source itself). Memoised.
158
179
  def padded_entity
159
180
  @padded_entity ||=
@@ -237,6 +258,27 @@ class CAWindowIterator < CAIterator
237
258
  end
238
259
  end
239
260
 
261
+ # How many of one axis's window offsets fall inside the source, per anchor.
262
+ # A window away from the edges holds all of them, so the vector is the window
263
+ # width everywhere but the two ends, and only the ends -- at most one window
264
+ # width of cells each -- are counted out.
265
+ def axis_cell_counts (k)
266
+ length = @shape[k]
267
+ first = @ranges[k].begin
268
+ last = @ranges[k].end
269
+ counts = CArray.int64(length)
270
+ counts[] = @widths[k]
271
+ truncated_at_the_start = [[-first, 0].max, length].min
272
+ truncated_at_the_end = [[length - last, 0].max, 0].max
273
+ indices = (0...truncated_at_the_start).to_a | (truncated_at_the_end...length).to_a
274
+ indices.each do |i|
275
+ low = [first, -i].max
276
+ high = [last, length - 1 - i].min
277
+ counts[i] = [high - low + 1, 0].max
278
+ end
279
+ counts
280
+ end
281
+
240
282
  # An index list of length `nd` that is `nil` (full range) on every axis
241
283
  # except `ax`, which is pinned to `k`.
242
284
  def axis_selector (nd, ax, k)
@@ -251,7 +293,7 @@ class CAWindowIterator < CAIterator
251
293
  #
252
294
  # A per-window fold to one value over the trailing window axes is exactly a
253
295
  # core per-axis reduction over those axes, so every reduction delegates to
254
- # `sliding_view.<op>(axis: window_axes, ...)`. This inherits the core dtype,
296
+ # `sliding_view.<op>(axis: window_axes, ...)`. This inherits the core data type,
255
297
  # mask, empty / all-masked (identity vs UNDEF) and epsilon-close contracts
256
298
  # unchanged. `min_count:` / `fill_value:` pass straight to the core (the
257
299
  # boundary strictness + result fill knobs).
@@ -259,17 +301,28 @@ class CAWindowIterator < CAIterator
259
301
  # @overload sum(min_count: nil, fill_value: nil)
260
302
  # Rolling sum, delegating to `sliding_view.sum(axis: window_axes)`.
261
303
  # @return [CArray] reference-shaped (or shrunk, for :truncate)
304
+ # @overload accumulate(min_count: nil, fill_value: nil)
305
+ # Rolling sum in the source's own data type, wrapping at its width, as the
306
+ # core `accumulate` does. `sum` answers in the type the core promotes to
307
+ # (float64 for integers), which for a window over bytes moves eight times
308
+ # the bytes; this is the spelling for staying in the type when the window
309
+ # cannot overflow it.
310
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
262
311
  # The rest are analogous: prod / mean / min / max, sample and population
263
312
  # variance / stddev, all / any, fused minmax, and the window-local position
264
313
  # min_index / max_index (index within the window axes).
265
- [:sum, :prod, :mean, :min, :max, :variance, :stddev, :all, :any,
314
+ [:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev, :all, :any,
266
315
  :variancep, :stddevp, :minmax, :min_index, :max_index].each do |op|
267
- define_method(op) do |min_count: nil, fill_value: nil|
268
- kw = {}
269
- kw[:min_count] = min_count unless min_count.nil?
270
- kw[:fill_value] = fill_value unless fill_value.nil?
271
- sliding_view.send(op, axis: @window_axes, **kw)
272
- end
316
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
317
+ def #{op} (min_count: nil, fill_value: nil)
318
+ folded = fold_by_offset(:#{op}, min_count, fill_value)
319
+ return folded unless folded.nil?
320
+ kw = {}
321
+ kw[:min_count] = min_count unless min_count.nil?
322
+ kw[:fill_value] = fill_value unless fill_value.nil?
323
+ sliding_view.#{op}(axis: @window_axes, **kw)
324
+ end
325
+ RUBY
273
326
  end
274
327
 
275
328
  # @overload min_addr
@@ -290,6 +343,225 @@ class CAWindowIterator < CAIterator
290
343
 
291
344
  private
292
345
 
346
+ # ---- folding by offset instead of by anchor ---------------------------
347
+ #
348
+ # Delegating to `sliding_view.<op>(axis: window_axes)` folds the window axes,
349
+ # which are the innermost ones, so the core pays its per-fiber setup once per
350
+ # output cell -- about 10 ns, whatever the window holds. For a 3x3 window
351
+ # that setup is nine tenths of the time.
352
+ #
353
+ # The same fold can be run the other way round: walk the window offsets, and
354
+ # for each one add the whole shifted plane into an accumulator. Then the
355
+ # setup is paid once per offset rather than once per output cell, and every
356
+ # pass is a straight walk over contiguous memory. It costs one pass per
357
+ # offset, so it only pays while the window is small; the crossover moves with
358
+ # rank and data type, but stays above width 5 on every axis in every
359
+ # combination measured (see devel/PROPOSAL_WINDOW_OFFSET_ACCUMULATION.md).
360
+ #
361
+ # What is decided here is the order of the fold and which pad it reads; the
362
+ # arithmetic is the core's elementwise kernel, and the result data type is
363
+ # the core's answer for the same reduction (asked below, so it cannot drift).
364
+
365
+ # The in-place elementwise kernel that accumulates one offset, per operation.
366
+ # An operation absent here has no identity to accumulate from and always
367
+ # takes the delegating path.
368
+ OFFSET_FOLD = { :sum => :add!, :accumulate => :add!,
369
+ :prod => :mul!,
370
+ :min => :pmin!, :max => :pmax!,
371
+ :all => :and!, :any => :or! }.freeze
372
+
373
+ # Widest window this path takes on any one axis.
374
+ OFFSET_FOLD_MAX_WIDTH = 5
375
+
376
+ # Runs the fold by offset, or returns nil when this window is not one it can
377
+ # answer for -- in which case the caller delegates as before.
378
+ def fold_by_offset (op, min_count, fill_value = nil)
379
+ return nil unless op == :mean || OFFSET_FOLD.key?(op)
380
+ return nil if @widths.any? { |width| width > OFFSET_FOLD_MAX_WIDTH }
381
+ return nil if neutral_value(op).nil?
382
+
383
+ folded = op == :mean ? fold_mean : accumulate_offsets(op)
384
+ folded = mask_empty_windows(folded) if EMPTY_WINDOW_IS_UNDEFINED.include?(op)
385
+ folded = apply_min_count(folded, min_count)
386
+ # A `fill_value:` on the call replaces a result that came out undefined.
387
+ folded = folded.strip_mask(fill_value) if !fill_value.nil? && folded.has_mask?
388
+ folded
389
+ end
390
+
391
+ # The mean is the sum over the same offsets, divided by the number of cells
392
+ # each window folded. A window that folded nothing divides by one here and
393
+ # is masked out by {#mask_empty_windows} after.
394
+ def fold_mean
395
+ counts = window_cell_counts
396
+ divisor = counts
397
+ if counts.is_a?(CArray) && counts.min.zero?
398
+ divisor = counts.copy
399
+ divisor[counts.eq(0)] = 1
400
+ end
401
+ accumulate_offsets(:sum).to_type(offset_fold_data_type(:mean)) / divisor
402
+ end
403
+
404
+ # Walks the window offsets, accumulating each shifted plane into the result.
405
+ def accumulate_offsets (op, base = nil, kernel = nil, data_type = nil)
406
+ base ||= offset_fold_base(op)
407
+ kernel ||= OFFSET_FOLD.fetch(op)
408
+ data_type ||= offset_fold_data_type(op)
409
+ accumulator = nil
410
+ offset_grid.each do |offset|
411
+ plane = base[*@ndim.times.map { |k|
412
+ start = @origins[k] + offset[k]
413
+ start...(start + @shape[k])
414
+ }]
415
+ if accumulator.nil?
416
+ accumulator = CArray.new(data_type, @shape)
417
+ accumulator[] = plane
418
+ else
419
+ accumulator.send(kernel, plane)
420
+ end
421
+ end
422
+ accumulator
423
+ end
424
+
425
+ # These have no value to give for a window that folded nothing; the ones not
426
+ # listed answer with their identity, which the accumulation already holds.
427
+ EMPTY_WINDOW_IS_UNDEFINED = [:min, :max, :mean].freeze
428
+
429
+ def mask_empty_windows (folded)
430
+ return folded unless windows_can_be_empty?
431
+ folded[window_cell_counts.eq(0)] = UNDEF
432
+ folded
433
+ end
434
+
435
+ # Whether any window can come out holding nothing at all. A masked source
436
+ # can leave one empty anywhere. Without one, it takes a window that reaches
437
+ # past the array and does not cover its own anchor -- `windows(1..2)` at the
438
+ # far edge -- and the count on an axis falls away towards its ends, so the
439
+ # two ends are the only places to look.
440
+ def windows_can_be_empty?
441
+ return true if @source.has_mask?
442
+ return false if window_cell_counts.is_a?(Integer)
443
+ @ndim.times.any? do |k|
444
+ along = axis_cell_counts(k)
445
+ along[0].zero? || along[@shape[k] - 1].zero?
446
+ end
447
+ end
448
+
449
+ # `min_count` asks for a result only where the window held that many cells.
450
+ def apply_min_count (folded, min_count)
451
+ return folded if min_count.nil?
452
+ counts = window_cell_counts
453
+ if counts.is_a?(Integer)
454
+ folded[] = UNDEF if counts < min_count
455
+ else
456
+ folded[counts.lt(min_count)] = UNDEF
457
+ end
458
+ folded
459
+ end
460
+
461
+ # What stands in for a cell the fold must not see: a margin the boundary
462
+ # policy does not fill, or a masked cell of the source. For sum, prod, all
463
+ # and any that is the operation's identity. For min and max it is any value
464
+ # that cannot win, and the array's own extreme is one -- so no table of
465
+ # per-type limits is needed. Nil means there is none to be had (a source
466
+ # masked everywhere), and the caller delegates instead.
467
+ def neutral_value (op)
468
+ @neutral_value ||= {}
469
+ return @neutral_value[op] if @neutral_value.key?(op)
470
+ @neutral_value[op] =
471
+ case op
472
+ when :sum, :accumulate, :mean then 0
473
+ when :prod then 1
474
+ when :all then true
475
+ when :any then false
476
+ when :min then source_extreme(:max)
477
+ when :max then source_extreme(:min)
478
+ end
479
+ end
480
+
481
+ def source_extreme (op)
482
+ value = @source.send(op)
483
+ value.equal?(UNDEF) ? nil : value
484
+ end
485
+
486
+ # The buffer the offsets are read from: the source with its margins filled
487
+ # per the boundary policy, with any masked cell replaced by the neutral value
488
+ # -- an accumulation propagates a mask where the fold would skip it -- and in
489
+ # the type the result is accumulated in. Adding across two types runs a
490
+ # different kernel from adding within one, and how much slower that is
491
+ # depends on the pair, the working set and the compiler; converting once is
492
+ # one behaviour everywhere. It costs a buffer in the wider type, which for
493
+ # a `sum` over bytes is the one case where it does not pay.
494
+ def offset_fold_base (op)
495
+ @offset_fold_base ||= {}
496
+ @offset_fold_base[op] ||=
497
+ begin
498
+ padded =
499
+ if @source.has_mask?
500
+ pad_for(@source.strip_mask(neutral_value(op)), neutral_value(op))
501
+ elsif @bounds == :skip
502
+ pad_source(@source, @lefts, @rights, :constant, neutral_value(op))
503
+ else
504
+ padded_entity
505
+ end
506
+ wanted = offset_fold_data_type(op)
507
+ padded.data_type == wanted ? padded : padded.to_type(wanted)
508
+ end
509
+ end
510
+
511
+ # Pads `values` the way this window's boundary policy says, with `outside`
512
+ # standing in for the margin where the policy does not fill one.
513
+ def pad_for (values, outside)
514
+ case @bounds
515
+ when :truncate then values
516
+ when :nearest then pad_source(values, @lefts, @rights, :edge, nil)
517
+ when :constant then pad_source(values, @lefts, @rights, :constant, @fill_value)
518
+ else pad_source(values, @lefts, @rights, :constant, outside)
519
+ end
520
+ end
521
+
522
+ # How many cells each window holds. With an unmasked source this follows
523
+ # from the geometry: every boundary policy but `:skip` fills its margins with
524
+ # real values, so the count is the whole window and one Integer says it;
525
+ # `:skip` counts the in-bounds offsets, which is separable -- the count on
526
+ # one axis depends on that axis alone. A masked source has to be counted for
527
+ # real, by accumulating over the same offsets.
528
+ def window_cell_counts
529
+ return @window_cell_counts unless @window_cell_counts.nil?
530
+ @window_cell_counts =
531
+ if @source.has_mask?
532
+ accumulate_offsets(:sum, pad_for(@source.is_not_masked.int64, 0),
533
+ :add!, CA_INT64)
534
+ elsif @bounds != :skip
535
+ @widths.inject(:*)
536
+ else
537
+ counts = CArray.int64(*@shape)
538
+ counts[] = 1
539
+ @ndim.times do |k|
540
+ shape = Array.new(@ndim, 1)
541
+ shape[k] = @shape[k]
542
+ counts.mul!(axis_cell_counts(k).reshape(*shape))
543
+ end
544
+ counts
545
+ end
546
+ end
547
+
548
+ # The data type the delegating path would have produced, asked of the core
549
+ # itself so the two paths cannot disagree.
550
+ def offset_fold_data_type (op)
551
+ @offset_fold_data_type ||= {}
552
+ @offset_fold_data_type[op] ||=
553
+ CArray.new(@source.data_type, [1, 1]).send(op, axis: [1]).data_type
554
+ end
555
+
556
+ # Every offset within the window, as a list of per-axis positions into the
557
+ # padded buffer.
558
+ def offset_grid
559
+ @offset_grid ||=
560
+ @widths.map { |width| (0...width).to_a }
561
+ .inject { |grid, axis| grid.product(axis).map { |pair| Array(pair).flatten } }
562
+ .map { |offset| Array(offset) }
563
+ end
564
+
293
565
  # Source address of the per-anchor winner. The window-local flat index
294
566
  # (min_index / max_index) decomposes into per-axis window coordinates; the
295
567
  # source coordinate on each axis is anchor + offset + window-coordinate (the
@@ -303,7 +575,7 @@ class CAWindowIterator < CAIterator
303
575
  (@sndim - 1).downto(0) { |i| wstride[i] = acc; acc *= @widths[i] }
304
576
  sstride = Array.new(@sndim); acc = 1
305
577
  (@sndim - 1).downto(0) { |i| sstride[i] = acc; acc *= n[i] }
306
- lo = (@bounds == :truncate) ? Array.new(@sndim, 0) : @ranges.map(&:begin)
578
+ lo = (@bounds == :truncate) ? @origins : @ranges.map(&:begin)
307
579
  addr = CArray.int64(*@shape); addr[] = 0
308
580
  oob = CArray.boolean(*@shape); oob[] = 0
309
581
  (0...@sndim).each do |i|
data/lib/carray.rb CHANGED
@@ -32,11 +32,11 @@ require 'carray/math'
32
32
  # carray/clip_cast CIFY (2026-06-23): translated to ext/carray_cast.c
33
33
  require 'carray/complex' # real / imag accessors; MUST precede carray/lazy
34
34
  # (lazy aliases real/imag at load time)
35
- require 'carray/lazy' # PROPOSAL_LAZY_ELEMENTWISE_VIEW Phase 1 P.1.2
35
+ require 'carray/lazy' # the lazy elementwise view layer
36
36
  # carray/face.rb was deleted (the Phase 1 skeleton CArray::Face module
37
37
  # became dead weight, replaced by C-layer macro deploy +
38
38
  # copy_state/storage_to_scalar convention).
39
- require 'carray/time' # CATime / CATimedelta (PROPOSAL_CAFACE_PHASE_2 F.2.4)
39
+ require 'carray/time' # CATime / CATimedelta
40
40
  # carray/methods/* (bincount / broadcast / gather_nd+put_nd) are small
41
41
  # single-feature method files, loaded lazily via autoload_carray.
42
42
  require 'carray/iterator'
@@ -44,9 +44,9 @@ require 'carray/iterator'
44
44
  # stubs). Nothing in the eager core references CAStruct/CAUnion, so programs
45
45
  # that only use numeric arrays never pay its load cost.
46
46
  require 'carray/string_operation_extension' # shared StringOperationMixin (must precede the Faces)
47
- require 'carray/const_string' # PROPOSAL_CATEXT.md T.2 — CAConstString construction surface
48
- require 'carray/string' # PROPOSAL_STRING_FACE_TRIO.md P.1 — CAString construction surface
49
- require 'carray/fixlen_string' # PROPOSAL_STRING_FACE_TRIO.md P.1 — CAFixlenString construction surface
47
+ require 'carray/const_string' # CAConstString construction surface
48
+ require 'carray/string' # CAString construction surface
49
+ require 'carray/fixlen_string' # CAFixlenString construction surface
50
50
  # CArray.format lives in carray/methods/string_format.rb, autoloaded on first call
51
51
 
52
52
  # carray/frame is loaded lazily via autoload_carray (CAFrame / GroupedFrame