carray 3.0.0 → 3.0.2

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +845 -3
  4. data/{NEWS.md → CHANGELOG.v1.md} +1 -1
  5. data/README.md +33 -33
  6. data/carray.gemspec +12 -8
  7. data/ext/ca_axis_dispatch.c +33 -4
  8. data/ext/ca_axis_group.c +256 -114
  9. data/ext/ca_binop_dispatch.c +5 -6
  10. data/ext/ca_binop_dispatch.h +0 -7
  11. data/ext/ca_categorical_iterator.c +121 -67
  12. data/ext/ca_for_each_element.h +6 -8
  13. data/ext/ca_group_iter.c +1 -1
  14. data/ext/ca_kernel_iterator.c +386 -74
  15. data/ext/ca_kernel_iterator.h +145 -45
  16. data/ext/ca_obj_array.c +62 -20
  17. data/ext/ca_obj_bincmp.c +57 -27
  18. data/ext/ca_obj_binop.c +86 -30
  19. data/ext/ca_obj_block.c +4 -4
  20. data/ext/ca_obj_const_string.c +88 -27
  21. data/ext/ca_obj_face.c +104 -0
  22. data/ext/ca_obj_face.h +48 -1
  23. data/ext/ca_obj_fake.c +11 -0
  24. data/ext/ca_obj_fixlen_string.c +21 -6
  25. data/ext/ca_obj_grid.c +11 -3
  26. data/ext/ca_obj_meld.c +125 -22
  27. data/ext/ca_obj_moncmp.c +33 -14
  28. data/ext/ca_obj_monop.c +19 -7
  29. data/ext/ca_obj_object.c +107 -4
  30. data/ext/ca_obj_record.c +3 -1
  31. data/ext/ca_obj_refer.c +30 -6
  32. data/ext/ca_obj_roll.c +13 -5
  33. data/ext/ca_obj_select.c +49 -34
  34. data/ext/ca_obj_select_axis.c +16 -6
  35. data/ext/ca_obj_shift.c +3 -3
  36. data/ext/ca_obj_stack.c +3 -8
  37. data/ext/ca_obj_stride.c +178 -24
  38. data/ext/ca_obj_string.c +11 -5
  39. data/ext/ca_obj_tile.c +13 -5
  40. data/ext/ca_obj_time.c +3 -1
  41. data/ext/ca_obj_timedelta.c +3 -1
  42. data/ext/ca_obj_transpose.c +1 -1
  43. data/ext/ca_obj_triop.c +62 -23
  44. data/ext/ca_obj_window.c +94 -19
  45. data/ext/ca_op_cmplx64.h +123 -0
  46. data/ext/ca_op_ipower.c +1 -5
  47. data/ext/ca_rng_normal.h +42 -0
  48. data/ext/ca_rng_xoshiro256pp.h +105 -0
  49. data/ext/ca_sort_kernels.h +5 -5
  50. data/ext/ca_sweep_engine.c +362 -155
  51. data/ext/ca_sweep_engine.h +39 -9
  52. data/ext/ca_transform_common.c +7 -0
  53. data/ext/carray.h +148 -132
  54. data/ext/carray_access.c +83 -49
  55. data/ext/carray_address_basis.c +590 -0
  56. data/ext/carray_bincount.c +8 -8
  57. data/ext/carray_broadcast.c +102 -9
  58. data/ext/carray_build_flags.h +3 -0
  59. data/ext/carray_call_cfunc.c +2010 -335
  60. data/ext/carray_call_cfunc.h +153 -0
  61. data/ext/carray_cast.c +184 -72
  62. data/ext/carray_conversion.c +22 -22
  63. data/ext/carray_copy.c +67 -32
  64. data/ext/carray_core.c +140 -13
  65. data/ext/carray_count.c +10 -11
  66. data/ext/carray_element.c +7 -7
  67. data/ext/carray_factorize.c +72 -51
  68. data/ext/carray_hold.c +1 -1
  69. data/ext/carray_index_classifier.c +7 -21
  70. data/ext/carray_internal.h +36 -3
  71. data/ext/carray_kernels_bincmp.c +1 -0
  72. data/ext/carray_kernels_binop.c +320 -298
  73. data/ext/carray_kernels_init.c +1095 -0
  74. data/ext/carray_kernels_map.c +1 -0
  75. data/ext/carray_kernels_moncmp.c +1 -0
  76. data/ext/carray_kernels_monop.c +412 -411
  77. data/ext/carray_kernels_reduce_aggregate.c +256 -87
  78. data/ext/carray_kernels_reduce_boolean.c +1 -0
  79. data/ext/carray_kernels_reduce_cumulative.c +324 -54
  80. data/ext/carray_kernels_reduce_extreme.c +605 -58
  81. data/ext/carray_kernels_reduce_variance.c +1 -0
  82. data/ext/carray_kernels_scan.c +5 -4
  83. data/ext/carray_kernels_search.c +187 -106
  84. data/ext/carray_kernels_sort.c +1 -0
  85. data/ext/carray_kernels_triop.c +1 -0
  86. data/ext/carray_lazy.c +175 -5
  87. data/ext/carray_loop.c +7 -1
  88. data/ext/carray_mask.c +29 -13
  89. data/ext/carray_median_percentile.c +56 -1
  90. data/ext/carray_memory_view.c +46 -21
  91. data/ext/carray_operator.c +50 -53
  92. data/ext/carray_order.c +1 -1
  93. data/ext/carray_partition.c +4 -4
  94. data/ext/carray_random.c +391 -47
  95. data/ext/carray_scatter.c +1 -1
  96. data/ext/carray_slab.c +13 -0
  97. data/ext/carray_sort.c +22 -24
  98. data/ext/carray_sort_kernel.c +10 -10
  99. data/ext/carray_test.c +1 -1
  100. data/ext/extconf.rb +18 -0
  101. data/ext/mk_call_cfunc.rb +323 -145
  102. data/ext/mkkernel.rb +651 -129
  103. data/ext/ruby_carray.c +28 -17
  104. data/ext/version.h +4 -4
  105. data/lib/carray/autoload_carray.rb +24 -3
  106. data/lib/carray/autoload_method_extension.rb +14 -1
  107. data/lib/carray/axis_group.rb +84 -7
  108. data/lib/carray/basics.rb +63 -59
  109. data/lib/carray/bincount_nd.rb +12 -12
  110. data/lib/carray/block_iterator.rb +111 -25
  111. data/lib/carray/categorical.rb +154 -37
  112. data/lib/carray/categorical_iterator.rb +300 -151
  113. data/lib/carray/conditional.rb +14 -14
  114. data/lib/carray/const_string.rb +131 -27
  115. data/lib/carray/construct.rb +61 -0
  116. data/lib/carray/core_extensions.rb +62 -5
  117. data/lib/carray/data_type_extension.rb +21 -1
  118. data/lib/carray/data_type_limits.rb +91 -0
  119. data/lib/carray/fixlen_string.rb +1 -1
  120. data/lib/carray/frame/concat.rb +3 -3
  121. data/lib/carray/frame/convert.rb +1 -1
  122. data/lib/carray/frame/csv_parser.rb +11 -4
  123. data/lib/carray/frame/frame.rb +85 -21
  124. data/lib/carray/frame/group.rb +36 -3
  125. data/lib/carray/frame/io.rb +224 -15
  126. data/lib/carray/frame/records.rb +18 -4
  127. data/lib/carray/frame/sort.rb +1 -1
  128. data/lib/carray/frame/verbs.rb +32 -12
  129. data/lib/carray/fuse_source.rb +123 -0
  130. data/lib/carray/fusion.rb +218 -0
  131. data/lib/carray/histogram.rb +16 -16
  132. data/lib/carray/inspect.rb +43 -16
  133. data/lib/carray/iterator.rb +147 -3
  134. data/lib/carray/lazy.rb +88 -73
  135. data/lib/carray/mask_gap_fill.rb +3 -1
  136. data/lib/carray/meld_reduce.rb +2 -2
  137. data/lib/carray/methods/align_addr.rb +1 -1
  138. data/lib/carray/methods/composition.rb +1 -1
  139. data/lib/carray/methods/discovery_along.rb +74 -0
  140. data/lib/carray/methods/factorize.rb +50 -0
  141. data/lib/carray/methods/is_in.rb +25 -14
  142. data/lib/carray/methods/locate_addr.rb +81 -3
  143. data/lib/carray/methods/mask_duplicates.rb +36 -2
  144. data/lib/carray/methods/meshgrid.rb +4 -5
  145. data/lib/carray/methods/mode.rb +2 -2
  146. data/lib/carray/methods/nunique.rb +23 -2
  147. data/lib/carray/methods/repeat.rb +110 -0
  148. data/lib/carray/methods/snap.rb +7 -2
  149. data/lib/carray/methods/unique.rb +44 -4
  150. data/lib/carray/methods/value_counts.rb +2 -2
  151. data/lib/carray/rng.rb +86 -0
  152. data/lib/carray/runtime.rb +0 -19
  153. data/lib/carray/slab_iterator.rb +77 -19
  154. data/lib/carray/string_operation_extension.rb +10 -6
  155. data/lib/carray/time.rb +906 -491
  156. data/lib/carray/window_iterator.rb +421 -27
  157. data/lib/carray.rb +7 -5
  158. data/yard-stubs/ca_obj_array.rb +385 -0
  159. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  160. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  161. data/yard-stubs/ca_obj_block.rb +68 -0
  162. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  163. data/yard-stubs/ca_obj_fake.rb +31 -0
  164. data/yard-stubs/ca_obj_farray.rb +32 -0
  165. data/yard-stubs/ca_obj_field.rb +45 -0
  166. data/yard-stubs/ca_obj_grid.rb +35 -0
  167. data/yard-stubs/ca_obj_refer.rb +72 -0
  168. data/yard-stubs/ca_obj_roll.rb +45 -0
  169. data/yard-stubs/ca_obj_shift.rb +43 -0
  170. data/yard-stubs/ca_obj_stride.rb +181 -0
  171. data/yard-stubs/ca_obj_tile.rb +29 -0
  172. data/yard-stubs/ca_obj_transpose.rb +40 -0
  173. data/yard-stubs/ca_obj_window.rb +57 -0
  174. data/yard-stubs/carray_access.rb +131 -0
  175. data/yard-stubs/carray_attribute.rb +246 -0
  176. data/yard-stubs/carray_broadcast.rb +37 -0
  177. data/yard-stubs/carray_cast.rb +489 -0
  178. data/yard-stubs/carray_class.rb +65 -0
  179. data/yard-stubs/carray_conversion.rb +76 -0
  180. data/yard-stubs/carray_copy.rb +79 -0
  181. data/yard-stubs/carray_core.rb +34 -0
  182. data/yard-stubs/carray_count.rb +84 -0
  183. data/yard-stubs/carray_element.rb +108 -0
  184. data/yard-stubs/carray_generate.rb +66 -0
  185. data/yard-stubs/carray_lazy.rb +228 -0
  186. data/yard-stubs/carray_loop.rb +140 -0
  187. data/yard-stubs/carray_mask.rb +259 -0
  188. data/yard-stubs/carray_math.rb +1615 -0
  189. data/yard-stubs/carray_mathfunc.rb +45 -0
  190. data/yard-stubs/carray_median_percentile.rb +103 -0
  191. data/yard-stubs/carray_memory_view.rb +163 -0
  192. data/yard-stubs/carray_order.rb +252 -0
  193. data/yard-stubs/carray_random.rb +89 -0
  194. data/yard-stubs/carray_scatter.rb +106 -0
  195. data/yard-stubs/carray_slab.rb +59 -0
  196. data/yard-stubs/carray_sort.rb +165 -0
  197. data/yard-stubs/carray_test.rb +85 -0
  198. data/yard-stubs/carray_undef.rb +64 -0
  199. data/yard-stubs/carray_utils.rb +97 -0
  200. data/yard-stubs/ruby_carray.rb +193 -0
  201. metadata +67 -10
  202. 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
  #
@@ -83,10 +83,10 @@ class CAWindowIterator < CAIterator
83
83
  def initialize (source, *ranges, bounds: :skip, fill_value: nil)
84
84
  if source.is_a?(CArray) && source.obj_type == CA_OBJ_WINDOW
85
85
  # Backward-compat: read geometry from a CAWindow view built by #window.
86
- # start[i] = lo, dim[i] (window width) = w, so hi = lo + w - 1.
86
+ # start[i] = lo, shape[i] (window width) = w, so hi = lo + w - 1.
87
87
  win = source
88
88
  @source = win.parent
89
- widths = win.count
89
+ widths = win.shape
90
90
  @ranges = win.start.each_with_index.map { |lo, i| lo..(lo + widths[i] - 1) }
91
91
  # The legacy #window default is FILL (constant), whose value is the
92
92
  # view's fill_value; map that to a :constant margin.
@@ -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,25 +293,130 @@ 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).
258
300
 
259
- # @overload sum(min_count: nil, fill_value: nil)
260
- # Rolling sum, delegating to `sliding_view.sum(axis: window_axes)`.
301
+ # @!method sum(min_count: nil, fill_value: nil)
302
+ # Rolling sum.
303
+ # @param min_count [Integer, nil] fewest cells that must be present
304
+ # for a result; a piece with fewer comes back masked.
305
+ # @param fill_value [Object, nil] value to put in place of a masked
306
+ # result instead of leaving it masked.
307
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
308
+ # @!method accumulate(min_count: nil, fill_value: nil)
309
+ # Rolling sum kept in the source's own data type, wrapping at its
310
+ # width, as the core `accumulate` does -- `sum` answers in the type
311
+ # the core promotes to (float64 for integers).
312
+ # @param min_count [Integer, nil] fewest cells that must be present
313
+ # for a result; a piece with fewer comes back masked.
314
+ # @param fill_value [Object, nil] value to put in place of a masked
315
+ # result instead of leaving it masked.
316
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
317
+ # @!method prod(min_count: nil, fill_value: nil)
318
+ # Rolling product.
319
+ # @param min_count [Integer, nil] fewest cells that must be present
320
+ # for a result; a piece with fewer comes back masked.
321
+ # @param fill_value [Object, nil] value to put in place of a masked
322
+ # result instead of leaving it masked.
323
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
324
+ # @!method mean(min_count: nil, fill_value: nil)
325
+ # Rolling arithmetic mean.
326
+ # @param min_count [Integer, nil] fewest cells that must be present
327
+ # for a result; a piece with fewer comes back masked.
328
+ # @param fill_value [Object, nil] value to put in place of a masked
329
+ # result instead of leaving it masked.
330
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
331
+ # @!method min(min_count: nil, fill_value: nil)
332
+ # Rolling minimum.
333
+ # @param min_count [Integer, nil] fewest cells that must be present
334
+ # for a result; a piece with fewer comes back masked.
335
+ # @param fill_value [Object, nil] value to put in place of a masked
336
+ # result instead of leaving it masked.
337
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
338
+ # @!method max(min_count: nil, fill_value: nil)
339
+ # Rolling maximum.
340
+ # @param min_count [Integer, nil] fewest cells that must be present
341
+ # for a result; a piece with fewer comes back masked.
342
+ # @param fill_value [Object, nil] value to put in place of a masked
343
+ # result instead of leaving it masked.
344
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
345
+ # @!method variance(min_count: nil, fill_value: nil)
346
+ # Rolling sample variance (divisor `n - 1`).
347
+ # @param min_count [Integer, nil] fewest cells that must be present
348
+ # for a result; a piece with fewer comes back masked.
349
+ # @param fill_value [Object, nil] value to put in place of a masked
350
+ # result instead of leaving it masked.
261
351
  # @return [CArray] reference-shaped (or shrunk, for :truncate)
262
- # The rest are analogous: prod / mean / min / max, sample and population
263
- # variance / stddev, all / any, fused minmax, and the window-local position
264
- # min_index / max_index (index within the window axes).
265
- [:sum, :prod, :mean, :min, :max, :variance, :stddev, :all, :any,
352
+ # @!method stddev(min_count: nil, fill_value: nil)
353
+ # Rolling sample standard deviation (divisor `n - 1`).
354
+ # @param min_count [Integer, nil] fewest cells that must be present
355
+ # for a result; a piece with fewer comes back masked.
356
+ # @param fill_value [Object, nil] value to put in place of a masked
357
+ # result instead of leaving it masked.
358
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
359
+ # @!method all(min_count: nil, fill_value: nil)
360
+ # Whether every cell of each rolling is true.
361
+ # @param min_count [Integer, nil] fewest cells that must be present
362
+ # for a result; a piece with fewer comes back masked.
363
+ # @param fill_value [Object, nil] value to put in place of a masked
364
+ # result instead of leaving it masked.
365
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
366
+ # @!method any(min_count: nil, fill_value: nil)
367
+ # Whether any cell of each rolling is true.
368
+ # @param min_count [Integer, nil] fewest cells that must be present
369
+ # for a result; a piece with fewer comes back masked.
370
+ # @param fill_value [Object, nil] value to put in place of a masked
371
+ # result instead of leaving it masked.
372
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
373
+ # @!method variancep(min_count: nil, fill_value: nil)
374
+ # Rolling population variance (divisor `n`).
375
+ # @param min_count [Integer, nil] fewest cells that must be present
376
+ # for a result; a piece with fewer comes back masked.
377
+ # @param fill_value [Object, nil] value to put in place of a masked
378
+ # result instead of leaving it masked.
379
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
380
+ # @!method stddevp(min_count: nil, fill_value: nil)
381
+ # Rolling population standard deviation (divisor `n`).
382
+ # @param min_count [Integer, nil] fewest cells that must be present
383
+ # for a result; a piece with fewer comes back masked.
384
+ # @param fill_value [Object, nil] value to put in place of a masked
385
+ # result instead of leaving it masked.
386
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
387
+ # @!method minmax(min_count: nil, fill_value: nil)
388
+ # Rolling minimum and maximum, found in one pass.
389
+ # @param min_count [Integer, nil] fewest cells that must be present
390
+ # for a result; a piece with fewer comes back masked.
391
+ # @param fill_value [Object, nil] value to put in place of a masked
392
+ # result instead of leaving it masked.
393
+ # @return [Array<CArray>] the pair `[min, max]`, reference-shaped (or shrunk, for :truncate)
394
+ # @!method min_index(min_count: nil, fill_value: nil)
395
+ # Rolling position of the minimum, local to the window axes.
396
+ # @param min_count [Integer, nil] fewest cells that must be present
397
+ # for a result; a piece with fewer comes back masked.
398
+ # @param fill_value [Object, nil] value to put in place of a masked
399
+ # result instead of leaving it masked.
400
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
401
+ # @!method max_index(min_count: nil, fill_value: nil)
402
+ # Rolling position of the maximum, local to the window axes.
403
+ # @param min_count [Integer, nil] fewest cells that must be present
404
+ # for a result; a piece with fewer comes back masked.
405
+ # @param fill_value [Object, nil] value to put in place of a masked
406
+ # result instead of leaving it masked.
407
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
408
+ [:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev, :all, :any,
266
409
  :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
410
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
411
+ def #{op} (min_count: nil, fill_value: nil)
412
+ folded = fold_by_offset(:#{op}, min_count, fill_value)
413
+ return folded unless folded.nil?
414
+ kw = {}
415
+ kw[:min_count] = min_count unless min_count.nil?
416
+ kw[:fill_value] = fill_value unless fill_value.nil?
417
+ sliding_view.#{op}(axis: @window_axes, **kw)
418
+ end
419
+ RUBY
273
420
  end
274
421
 
275
422
  # @overload min_addr
@@ -290,6 +437,225 @@ class CAWindowIterator < CAIterator
290
437
 
291
438
  private
292
439
 
440
+ # ---- folding by offset instead of by anchor ---------------------------
441
+ #
442
+ # Delegating to `sliding_view.<op>(axis: window_axes)` folds the window axes,
443
+ # which are the innermost ones, so the core pays its per-fiber setup once per
444
+ # output cell -- about 10 ns, whatever the window holds. For a 3x3 window
445
+ # that setup is nine tenths of the time.
446
+ #
447
+ # The same fold can be run the other way round: walk the window offsets, and
448
+ # for each one add the whole shifted plane into an accumulator. Then the
449
+ # setup is paid once per offset rather than once per output cell, and every
450
+ # pass is a straight walk over contiguous memory. It costs one pass per
451
+ # offset, so it only pays while the window is small; the crossover moves with
452
+ # rank and data type, but stays above width 5 on every axis in every
453
+ # combination measured (see devel/PROPOSAL_WINDOW_OFFSET_ACCUMULATION.md).
454
+ #
455
+ # What is decided here is the order of the fold and which pad it reads; the
456
+ # arithmetic is the core's elementwise kernel, and the result data type is
457
+ # the core's answer for the same reduction (asked below, so it cannot drift).
458
+
459
+ # The in-place elementwise kernel that accumulates one offset, per operation.
460
+ # An operation absent here has no identity to accumulate from and always
461
+ # takes the delegating path.
462
+ OFFSET_FOLD = { :sum => :add!, :accumulate => :add!,
463
+ :prod => :mul!,
464
+ :min => :pmin!, :max => :pmax!,
465
+ :all => :and!, :any => :or! }.freeze
466
+
467
+ # Widest window this path takes on any one axis.
468
+ OFFSET_FOLD_MAX_WIDTH = 5
469
+
470
+ # Runs the fold by offset, or returns nil when this window is not one it can
471
+ # answer for -- in which case the caller delegates as before.
472
+ def fold_by_offset (op, min_count, fill_value = nil)
473
+ return nil unless op == :mean || OFFSET_FOLD.key?(op)
474
+ return nil if @widths.any? { |width| width > OFFSET_FOLD_MAX_WIDTH }
475
+ return nil if neutral_value(op).nil?
476
+
477
+ folded = op == :mean ? fold_mean : accumulate_offsets(op)
478
+ folded = mask_empty_windows(folded) if EMPTY_WINDOW_IS_UNDEFINED.include?(op)
479
+ folded = apply_min_count(folded, min_count)
480
+ # A `fill_value:` on the call replaces a result that came out undefined.
481
+ folded = folded.strip_mask(fill_value) if !fill_value.nil? && folded.has_mask?
482
+ folded
483
+ end
484
+
485
+ # The mean is the sum over the same offsets, divided by the number of cells
486
+ # each window folded. A window that folded nothing divides by one here and
487
+ # is masked out by {#mask_empty_windows} after.
488
+ def fold_mean
489
+ counts = window_cell_counts
490
+ divisor = counts
491
+ if counts.is_a?(CArray) && counts.min.zero?
492
+ divisor = counts.copy
493
+ divisor[counts.eq(0)] = 1
494
+ end
495
+ accumulate_offsets(:sum).to_type(offset_fold_data_type(:mean)) / divisor
496
+ end
497
+
498
+ # Walks the window offsets, accumulating each shifted plane into the result.
499
+ def accumulate_offsets (op, base = nil, kernel = nil, data_type = nil)
500
+ base ||= offset_fold_base(op)
501
+ kernel ||= OFFSET_FOLD.fetch(op)
502
+ data_type ||= offset_fold_data_type(op)
503
+ accumulator = nil
504
+ offset_grid.each do |offset|
505
+ plane = base[*@ndim.times.map { |k|
506
+ start = @origins[k] + offset[k]
507
+ start...(start + @shape[k])
508
+ }]
509
+ if accumulator.nil?
510
+ accumulator = CArray.new(data_type, @shape)
511
+ accumulator[] = plane
512
+ else
513
+ accumulator.send(kernel, plane)
514
+ end
515
+ end
516
+ accumulator
517
+ end
518
+
519
+ # These have no value to give for a window that folded nothing; the ones not
520
+ # listed answer with their identity, which the accumulation already holds.
521
+ EMPTY_WINDOW_IS_UNDEFINED = [:min, :max, :mean].freeze
522
+
523
+ def mask_empty_windows (folded)
524
+ return folded unless windows_can_be_empty?
525
+ folded[window_cell_counts.eq(0)] = UNDEF
526
+ folded
527
+ end
528
+
529
+ # Whether any window can come out holding nothing at all. A masked source
530
+ # can leave one empty anywhere. Without one, it takes a window that reaches
531
+ # past the array and does not cover its own anchor -- `windows(1..2)` at the
532
+ # far edge -- and the count on an axis falls away towards its ends, so the
533
+ # two ends are the only places to look.
534
+ def windows_can_be_empty?
535
+ return true if @source.has_mask?
536
+ return false if window_cell_counts.is_a?(Integer)
537
+ @ndim.times.any? do |k|
538
+ along = axis_cell_counts(k)
539
+ along[0].zero? || along[@shape[k] - 1].zero?
540
+ end
541
+ end
542
+
543
+ # `min_count` asks for a result only where the window held that many cells.
544
+ def apply_min_count (folded, min_count)
545
+ return folded if min_count.nil?
546
+ counts = window_cell_counts
547
+ if counts.is_a?(Integer)
548
+ folded[] = UNDEF if counts < min_count
549
+ else
550
+ folded[counts.lt(min_count)] = UNDEF
551
+ end
552
+ folded
553
+ end
554
+
555
+ # What stands in for a cell the fold must not see: a margin the boundary
556
+ # policy does not fill, or a masked cell of the source. For sum, prod, all
557
+ # and any that is the operation's identity. For min and max it is any value
558
+ # that cannot win, and the array's own extreme is one -- so no table of
559
+ # per-type limits is needed. Nil means there is none to be had (a source
560
+ # masked everywhere), and the caller delegates instead.
561
+ def neutral_value (op)
562
+ @neutral_value ||= {}
563
+ return @neutral_value[op] if @neutral_value.key?(op)
564
+ @neutral_value[op] =
565
+ case op
566
+ when :sum, :accumulate, :mean then 0
567
+ when :prod then 1
568
+ when :all then true
569
+ when :any then false
570
+ when :min then source_extreme(:max)
571
+ when :max then source_extreme(:min)
572
+ end
573
+ end
574
+
575
+ def source_extreme (op)
576
+ value = @source.send(op)
577
+ value.equal?(UNDEF) ? nil : value
578
+ end
579
+
580
+ # The buffer the offsets are read from: the source with its margins filled
581
+ # per the boundary policy, with any masked cell replaced by the neutral value
582
+ # -- an accumulation propagates a mask where the fold would skip it -- and in
583
+ # the type the result is accumulated in. Adding across two types runs a
584
+ # different kernel from adding within one, and how much slower that is
585
+ # depends on the pair, the working set and the compiler; converting once is
586
+ # one behaviour everywhere. It costs a buffer in the wider type, which for
587
+ # a `sum` over bytes is the one case where it does not pay.
588
+ def offset_fold_base (op)
589
+ @offset_fold_base ||= {}
590
+ @offset_fold_base[op] ||=
591
+ begin
592
+ padded =
593
+ if @source.has_mask?
594
+ pad_for(@source.strip_mask(neutral_value(op)), neutral_value(op))
595
+ elsif @bounds == :skip
596
+ pad_source(@source, @lefts, @rights, :constant, neutral_value(op))
597
+ else
598
+ padded_entity
599
+ end
600
+ wanted = offset_fold_data_type(op)
601
+ padded.data_type == wanted ? padded : padded.to_type(wanted)
602
+ end
603
+ end
604
+
605
+ # Pads `values` the way this window's boundary policy says, with `outside`
606
+ # standing in for the margin where the policy does not fill one.
607
+ def pad_for (values, outside)
608
+ case @bounds
609
+ when :truncate then values
610
+ when :nearest then pad_source(values, @lefts, @rights, :edge, nil)
611
+ when :constant then pad_source(values, @lefts, @rights, :constant, @fill_value)
612
+ else pad_source(values, @lefts, @rights, :constant, outside)
613
+ end
614
+ end
615
+
616
+ # How many cells each window holds. With an unmasked source this follows
617
+ # from the geometry: every boundary policy but `:skip` fills its margins with
618
+ # real values, so the count is the whole window and one Integer says it;
619
+ # `:skip` counts the in-bounds offsets, which is separable -- the count on
620
+ # one axis depends on that axis alone. A masked source has to be counted for
621
+ # real, by accumulating over the same offsets.
622
+ def window_cell_counts
623
+ return @window_cell_counts unless @window_cell_counts.nil?
624
+ @window_cell_counts =
625
+ if @source.has_mask?
626
+ accumulate_offsets(:sum, pad_for(@source.is_not_masked.int64, 0),
627
+ :add!, CA_INT64)
628
+ elsif @bounds != :skip
629
+ @widths.inject(:*)
630
+ else
631
+ counts = CArray.int64(*@shape)
632
+ counts[] = 1
633
+ @ndim.times do |k|
634
+ shape = Array.new(@ndim, 1)
635
+ shape[k] = @shape[k]
636
+ counts.mul!(axis_cell_counts(k).reshape(*shape))
637
+ end
638
+ counts
639
+ end
640
+ end
641
+
642
+ # The data type the delegating path would have produced, asked of the core
643
+ # itself so the two paths cannot disagree.
644
+ def offset_fold_data_type (op)
645
+ @offset_fold_data_type ||= {}
646
+ @offset_fold_data_type[op] ||=
647
+ CArray.new(@source.data_type, [1, 1]).send(op, axis: [1]).data_type
648
+ end
649
+
650
+ # Every offset within the window, as a list of per-axis positions into the
651
+ # padded buffer.
652
+ def offset_grid
653
+ @offset_grid ||=
654
+ @widths.map { |width| (0...width).to_a }
655
+ .inject { |grid, axis| grid.product(axis).map { |pair| Array(pair).flatten } }
656
+ .map { |offset| Array(offset) }
657
+ end
658
+
293
659
  # Source address of the per-anchor winner. The window-local flat index
294
660
  # (min_index / max_index) decomposes into per-axis window coordinates; the
295
661
  # source coordinate on each axis is anchor + offset + window-coordinate (the
@@ -303,7 +669,7 @@ class CAWindowIterator < CAIterator
303
669
  (@sndim - 1).downto(0) { |i| wstride[i] = acc; acc *= @widths[i] }
304
670
  sstride = Array.new(@sndim); acc = 1
305
671
  (@sndim - 1).downto(0) { |i| sstride[i] = acc; acc *= n[i] }
306
- lo = (@bounds == :truncate) ? Array.new(@sndim, 0) : @ranges.map(&:begin)
672
+ lo = (@bounds == :truncate) ? @origins : @ranges.map(&:begin)
307
673
  addr = CArray.int64(*@shape); addr[] = 0
308
674
  oob = CArray.boolean(*@shape); oob[] = 0
309
675
  (0...@sndim).each do |i|
@@ -601,13 +967,41 @@ class CAWindowIterator < CAIterator
601
967
  "sort of source addresses is ill-defined."
602
968
  end
603
969
 
604
- # @overload cumsum
605
- # @overload cumprod
606
- # @overload cummax
607
- # @overload cummin
608
- # @overload cumcount
609
- # Not supported for a window iterator: a segment scan writes a per-cell
610
- # running statistic, which is single-valued only when each cell belongs to
970
+ # @!method cumsum
971
+ # Not supported for a window iterator. A segment scan writes a per-cell
972
+ # running sum, which is single-valued only when each cell belongs to
973
+ # exactly one piece. Overlapping windows put a cell in many windows, so
974
+ # there is no single running value. Raises NotImplementedError, exactly as
975
+ # {#map} / {#sort_addr} do (min / max reductions stay available: a single
976
+ # winner is well-defined).
977
+ # @raise [NotImplementedError]
978
+ # @!method cumprod
979
+ # Not supported for a window iterator. A segment scan writes a per-cell
980
+ # running product, which is single-valued only when each cell belongs to
981
+ # exactly one piece. Overlapping windows put a cell in many windows, so
982
+ # there is no single running value. Raises NotImplementedError, exactly as
983
+ # {#map} / {#sort_addr} do (min / max reductions stay available: a single
984
+ # winner is well-defined).
985
+ # @raise [NotImplementedError]
986
+ # @!method cummax
987
+ # Not supported for a window iterator. A segment scan writes a per-cell
988
+ # running maximum, which is single-valued only when each cell belongs to
989
+ # exactly one piece. Overlapping windows put a cell in many windows, so
990
+ # there is no single running value. Raises NotImplementedError, exactly as
991
+ # {#map} / {#sort_addr} do (min / max reductions stay available: a single
992
+ # winner is well-defined).
993
+ # @raise [NotImplementedError]
994
+ # @!method cummin
995
+ # Not supported for a window iterator. A segment scan writes a per-cell
996
+ # running minimum, which is single-valued only when each cell belongs to
997
+ # exactly one piece. Overlapping windows put a cell in many windows, so
998
+ # there is no single running value. Raises NotImplementedError, exactly as
999
+ # {#map} / {#sort_addr} do (min / max reductions stay available: a single
1000
+ # winner is well-defined).
1001
+ # @raise [NotImplementedError]
1002
+ # @!method cumcount
1003
+ # Not supported for a window iterator. A segment scan writes a per-cell
1004
+ # running count of present cells, which is single-valued only when each cell belongs to
611
1005
  # exactly one piece. Overlapping windows put a cell in many windows, so
612
1006
  # there is no single running value. Raises NotImplementedError, exactly as
613
1007
  # {#map} / {#sort_addr} do (min / max reductions stay available: a single
data/lib/carray.rb CHANGED
@@ -16,6 +16,7 @@ end
16
16
  # interpolation, so compute programs that never print a CArray skip it.
17
17
  require 'carray/construct'
18
18
  require 'carray/data_type_extension'
19
+ require 'carray/data_type_limits' # MIN / MAX / TINY / EPSILON on the typed classes
19
20
  # carray/stack is loaded lazily via autoload_carray (entry-method stubs).
20
21
  # CAStack is C-defined so its constant is always present; only the Ruby
21
22
  # composition surface (stack/meld/montage/split/append) needs the file.
@@ -29,14 +30,15 @@ require 'carray/meld_reduce' # CAMeld per-parent reduce fast path (sum/mean/m
29
30
  # carray/ordering CIFY (2026-06-21): translated to ext/carray_order.c
30
31
 
31
32
  require 'carray/math'
33
+ require 'carray/rng' # CArray::Rng: the generator its C is shared from
32
34
  # carray/clip_cast CIFY (2026-06-23): translated to ext/carray_cast.c
33
35
  require 'carray/complex' # real / imag accessors; MUST precede carray/lazy
34
36
  # (lazy aliases real/imag at load time)
35
- require 'carray/lazy' # PROPOSAL_LAZY_ELEMENTWISE_VIEW Phase 1 P.1.2
37
+ require 'carray/lazy' # the lazy elementwise view layer
36
38
  # carray/face.rb was deleted (the Phase 1 skeleton CArray::Face module
37
39
  # became dead weight, replaced by C-layer macro deploy +
38
40
  # copy_state/storage_to_scalar convention).
39
- require 'carray/time' # CATime / CATimedelta (PROPOSAL_CAFACE_PHASE_2 F.2.4)
41
+ require 'carray/time' # CATime / CATimedelta
40
42
  # carray/methods/* (bincount / broadcast / gather_nd+put_nd) are small
41
43
  # single-feature method files, loaded lazily via autoload_carray.
42
44
  require 'carray/iterator'
@@ -44,9 +46,9 @@ require 'carray/iterator'
44
46
  # stubs). Nothing in the eager core references CAStruct/CAUnion, so programs
45
47
  # that only use numeric arrays never pay its load cost.
46
48
  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
49
+ require 'carray/const_string' # CAConstString construction surface
50
+ require 'carray/string' # CAString construction surface
51
+ require 'carray/fixlen_string' # CAFixlenString construction surface
50
52
  # CArray.format lives in carray/methods/string_format.rb, autoloaded on first call
51
53
 
52
54
  # carray/frame is loaded lazily via autoload_carray (CAFrame / GroupedFrame