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
@@ -26,18 +26,18 @@
26
26
  # the mask contract (empty / all-masked -> identity for sum/prod, UNDEF for
27
27
  # ratios) carries through unchanged. Results are length-k CArrays aligned to
28
28
  # `cat.labels`; undefined slots are MASKED cells (never magic floats). Output
29
- # dtype and the empty / all-masked answer per method:
29
+ # data type and the empty / all-masked answer per method:
30
30
  #
31
31
  # elements -> int64, classified cells (incl. masked)
32
32
  # count / count_not_masked / count_masked / count(v) -> int64
33
- # sum -> value dtype, empty/all-masked = 0 (identity)
33
+ # sum -> value data type, empty/all-masked = 0 (identity)
34
34
  # prod -> float64, empty/all-masked = 1 (identity)
35
- # max / min -> value dtype, empty/all-masked = MASKED
35
+ # max / min -> value data type, empty/all-masked = MASKED
36
36
  # mean -> float64, empty/all-masked = MASKED
37
37
  # median / percentile -> float64, empty/all-masked = MASKED
38
38
  # variance / stddev (sample, ddof=1) -> float64, empty/all-masked = MASKED,
39
39
  # single value = 0.0 (n=1 contract)
40
- # all / any -> boolean (boolean value dtype only)
40
+ # all / any -> boolean (boolean value data type only)
41
41
  # labels -> cat.labels
42
42
  #
43
43
  # Generic iteration (the escape hatch for statistics not in the named surface),
@@ -125,7 +125,7 @@ class CACategoricalIterator < CAIterator
125
125
  # 1-D value there is no fiber structure to broadcast into, so a mismatch
126
126
  # is unrecoverable (preserves the old strict check). For higher-rank
127
127
  # value, defer validation to reduce time — check only that cat.ndim fits
128
- # one of the 3 axis: cases (§2.2 of PROPOSAL_CATEGORICAL_REDUCE_AXIS);
128
+ # one of the 3 axis: cases;
129
129
  # any no-axis reduce called on this iterator will surface the mismatch
130
130
  # because @grouped stays undefined.
131
131
  if value.ndim == 1 ||
@@ -217,21 +217,20 @@ class CACategoricalIterator < CAIterator
217
217
  # @return [CArray] length-k int64, aligned to {#labels}
218
218
  # @overload count(axis:)
219
219
  # No-arg + axis: = per-fiber per-category count_not_masked (shape [K, ...band]).
220
- # `count(v, axis:)` (value equality) and `count(UNDEF, axis:)` are deferred
221
- # to Phase 3 of PROPOSAL_CATEGORICAL_REDUCE_AXIS.
220
+ # `count(v, axis:)` (value equality) and `count(UNDEF, axis:)` are not
221
+ # implemented; use them without `axis:`.
222
222
  # @param axis [Integer]
223
223
  # @return [CArray]
224
224
  def count (*args, axis: nil)
225
225
  if axis
226
226
  return count_not_masked(axis: axis) if args.empty?
227
227
  raise NotImplementedError,
228
- "CACategoricalIterator#count(v, axis:) not yet implemented — " \
229
- "value-equality count with axis: deferred to Phase 3 of " \
230
- "PROPOSAL_CATEGORICAL_REDUCE_AXIS."
228
+ "CACategoricalIterator#count(v, axis:) is not implemented — " \
229
+ "value-equality count is available without axis:."
231
230
  end
232
231
  return count_not_masked if args.empty?
233
232
  # Delegate per group to CArray#count (handles count(UNDEF) -> masked count and
234
- # count(v) alike, with core's exact dtype equality). The group slice is a
233
+ # count(v) alike, with core's exact data type equality). The group slice is a
235
234
  # CABlock, whose own #count is the block geometry accessor, so dispatch
236
235
  # CArray#count explicitly. (Not fused: a value-equality reduceat would have
237
236
  # to reproduce core's cross-type / out-of-range equality exactly.)
@@ -244,70 +243,84 @@ class CACategoricalIterator < CAIterator
244
243
  # Empty categories are `0`.
245
244
  # @return [CArray]
246
245
  # @overload count_masked(axis:)
247
- # Deferred to Phase 3 of PROPOSAL_CATEGORICAL_REDUCE_AXIS (needs a separate
248
- # "assigned count" scatter that counts cells regardless of value mask).
246
+ # Not implemented; call it without `axis:`.
249
247
  # @param axis [Integer]
250
248
  # @return [CArray]
251
249
  def count_masked(axis: nil)
252
250
  if axis
253
251
  raise NotImplementedError,
254
- "CACategoricalIterator#count_masked(axis:) not yet implemented — " \
255
- "deferred to Phase 3 of PROPOSAL_CATEGORICAL_REDUCE_AXIS " \
256
- "(needs a separate value-mask-only scatter kernel)."
252
+ "CACategoricalIterator#count_masked(axis:) is not implemented — " \
253
+ "call it without axis:."
257
254
  end
258
255
  m = moments
259
256
  m ? @elements - m[:count] : per_category(CA_INT64) { |s| s.count_masked }
260
257
  end
261
258
 
262
259
  # @overload sum
263
- # Returns per-category sums in the value dtype. An empty or fully-masked
264
- # category sums the empty set, which is the additive identity `0`
265
- # (unmasked) the same contract as `CArray#sum` on an empty / all-masked
266
- # array.
260
+ # Returns per-category sums in the data type `CArray#sum` promotes the value
261
+ # to (float64 for an integer value). `accumulate` is the same fold kept in
262
+ # the value's own type. An empty or fully-masked category sums the empty
263
+ # set, which is the additive identity `0` (unmasked) — the same contract as
264
+ # `CArray#sum` on an empty / all-masked array.
267
265
  # @return [CArray]
268
266
  # @overload sum(axis:)
269
267
  # Returns per-category sums per fiber along `axis`. Cat may be 1-D (case
270
268
  # A, broadcasts across band axes), same rank as source (case B, per-fiber
271
269
  # independent classifier), or one rank less (band-only, constant along
272
- # reduce axis). Output shape = `[K, ...source.shape without axis]`. See
273
- # PROPOSAL_CATEGORICAL_REDUCE_AXIS.
270
+ # reduce axis). Output shape = `[K, ...source.shape without axis]`.
274
271
  # @param axis [Integer] reduce axis of the source value.
275
272
  # @return [CArray]
276
273
  def sum(axis: nil)
277
274
  return axis_sum(axis) if axis
278
275
  m = moments
279
- return per_category(@grouped.data_type) { |s| s.sum } unless m
280
- out = CArray.new(@grouped.data_type, [@k])
281
- out[] = m[:sum] # cast float64 sums -> value dtype (empty -> 0)
282
- out
276
+ return per_category(core_reduce_type(:sum)) { |s| s.sum } unless m
277
+ m[:sum].copy # the moments sum IS the core fold (empty -> 0.0)
278
+ end
279
+
280
+ # @overload accumulate
281
+ # Returns per-category sums folded in the value's own data type, wrapping at
282
+ # its width, as the core `accumulate` does. This is the exact in-type fold:
283
+ # `sum` reads its answer off a float64 moment and casts back, so it loses
284
+ # the low bits of a wide integer payload and does not wrap. An empty or
285
+ # fully-masked category accumulates the empty set, the additive identity `0`
286
+ # (unmasked).
287
+ # @return [CArray]
288
+ # @overload accumulate(axis:)
289
+ # Per-fiber per-category in-type sums along `axis`. Output shape =
290
+ # `[K, ...source.shape without axis]`.
291
+ # @param axis [Integer] reduce axis of the source value.
292
+ # @return [CArray]
293
+ def accumulate(axis: nil)
294
+ return axis_by_masked_copy(axis, :accumulate, core_reduce_type(:accumulate)) if axis
295
+ per_category(core_reduce_type(:accumulate)) { |s| s.accumulate }
283
296
  end
284
297
 
285
298
  # @overload max
286
- # Returns per-category maxima in the value dtype. Empty categories are
299
+ # Returns per-category maxima in the value data type. Empty categories are
287
300
  # MASKED.
288
301
  # @return [CArray]
289
302
  # @overload max(axis:)
290
- # Per-fiber per-category maxima along `axis` (h dtype, masked where empty).
303
+ # Per-fiber per-category maxima along `axis` (h's data type, masked where empty).
291
304
  # @param axis [Integer]
292
305
  # @return [CArray]
293
306
  def max(axis: nil)
294
307
  return axis_moments(axis)[:max] if axis
295
308
  m = moments
296
- m ? m[:max] : per_category(@grouped.data_type) { |s| s.max }
309
+ m ? m[:max] : per_category(core_reduce_type(:max)) { |s| s.max }
297
310
  end
298
311
 
299
312
  # @overload min
300
- # Returns per-category minima in the value dtype. Empty categories are
313
+ # Returns per-category minima in the value data type. Empty categories are
301
314
  # MASKED.
302
315
  # @return [CArray]
303
316
  # @overload min(axis:)
304
- # Per-fiber per-category minima along `axis` (h dtype, masked where empty).
317
+ # Per-fiber per-category minima along `axis` (h's data type, masked where empty).
305
318
  # @param axis [Integer]
306
319
  # @return [CArray]
307
320
  def min(axis: nil)
308
321
  return axis_moments(axis)[:min] if axis
309
322
  m = moments
310
- m ? m[:min] : per_category(@grouped.data_type) { |s| s.min }
323
+ m ? m[:min] : per_category(core_reduce_type(:min)) { |s| s.min }
311
324
  end
312
325
 
313
326
  # @overload mean
@@ -320,7 +333,7 @@ class CACategoricalIterator < CAIterator
320
333
  def mean(axis: nil)
321
334
  return axis_mean(axis) if axis
322
335
  m = moments
323
- return per_category(CA_FLOAT64) { |s| s.mean } unless m
336
+ return per_category(core_reduce_type(:mean)) { |s| s.mean } unless m
324
337
  cnt = m[:count]
325
338
  out = m[:sum] / cnt.float64 # count 0 -> NaN, masked next
326
339
  out[cnt.eq(0)] = UNDEF # empty / all-masked category -> MASKED
@@ -345,7 +358,7 @@ class CACategoricalIterator < CAIterator
345
358
  def percentile (p, axis: nil)
346
359
  axis_order_stat_defer!(:percentile) if axis
347
360
  unless MONOID_TYPES.include?(@grouped.data_type)
348
- return per_category(CA_FLOAT64) { |s| s.percentile(p) }
361
+ return per_category(core_reduce_type(:percentile, p)) { |s| s.percentile(p) }
349
362
  end
350
363
  out = CArray.float64(@k)
351
364
  @grouped.send(:__reduceat_percentile__, @offsets, p.to_f, out)
@@ -374,9 +387,9 @@ class CACategoricalIterator < CAIterator
374
387
  # sample variance.
375
388
  # @return [CArray]
376
389
  def variance(axis: nil)
377
- return axis_variance_family(axis, :variance) if axis
390
+ return axis_by_masked_copy(axis, :variance) if axis
378
391
  m = moments
379
- return per_category(CA_FLOAT64) { |s| s.variance } unless m
392
+ return per_category(core_reduce_type(:variance)) { |s| s.variance } unless m
380
393
  cnt = m[:count]
381
394
  means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1,
382
395
  out = CArray.float64(@k) # ignored by the kernel's n<2 guards)
@@ -390,9 +403,9 @@ class CACategoricalIterator < CAIterator
390
403
  # single-value `0.0`).
391
404
  # @return [CArray]
392
405
  def stddev(axis: nil)
393
- return axis_variance_family(axis, :stddev) if axis
406
+ return axis_by_masked_copy(axis, :stddev) if axis
394
407
  m = moments
395
- return per_category(CA_FLOAT64) { |s| s.stddev } unless m
408
+ return per_category(core_reduce_type(:stddev)) { |s| s.stddev } unless m
396
409
  variance.sqrt # sqrt propagates the n=0 mask
397
410
  end
398
411
 
@@ -408,7 +421,7 @@ class CACategoricalIterator < CAIterator
408
421
  # @return [CArray]
409
422
  def prod(axis: nil)
410
423
  return axis_prod(axis) if axis
411
- return per_category(CA_FLOAT64) { |s| s.prod } unless MONOID_TYPES.include?(@grouped.data_type)
424
+ return per_category(core_reduce_type(:prod)) { |s| s.prod } unless MONOID_TYPES.include?(@grouped.data_type)
412
425
  out = CArray.float64(@k)
413
426
  @grouped.send(:__reduceat_prod__, @offsets, out)
414
427
  out
@@ -417,7 +430,7 @@ class CACategoricalIterator < CAIterator
417
430
  # @overload all
418
431
  # Returns the per-category `all` as boolean (matching `CArray#all`): true
419
432
  # iff every present value is truthy (empty category -> true, vacuously).
420
- # The value dtype must be boolean, as for `CArray#all`.
433
+ # The value data type must be boolean, as for `CArray#all`.
421
434
  # @return [CArray]
422
435
  def all
423
436
  aa = all_any
@@ -427,7 +440,7 @@ class CACategoricalIterator < CAIterator
427
440
  # @overload any
428
441
  # Returns the per-category `any` as boolean (matching `CArray#any`): true
429
442
  # iff some present value is truthy (empty category -> false). The value
430
- # dtype must be boolean, as for `CArray#any`.
443
+ # data type must be boolean, as for `CArray#any`.
431
444
  # @return [CArray]
432
445
  def any
433
446
  aa = all_any
@@ -438,11 +451,11 @@ class CACategoricalIterator < CAIterator
438
451
 
439
452
  # @overload minmax
440
453
  # Returns the per-category `[min, max]` pair (each a length-k CArray in the
441
- # value dtype; empty categories MASKED), matching `CArray#minmax`. Both come
454
+ # value data type; empty categories MASKED), matching `CArray#minmax`. Both come
442
455
  # from the single cached moments pass.
443
456
  # @return [Array<CArray>]
444
457
  # @overload minmax(axis:)
445
- # Per-fiber `[min_ca, max_ca]` along `axis` (each shape [K, ...band], h dtype,
458
+ # Per-fiber `[min_ca, max_ca]` along `axis` (each shape [K, ...band], h's data type,
446
459
  # empty group cells MASKED). Ruby Array of two CArrays, not stacked.
447
460
  # @param axis [Integer]
448
461
  # @return [Array<CArray>]
@@ -458,9 +471,9 @@ class CACategoricalIterator < CAIterator
458
471
  # reuses the centred two-pass kernel with no extra walk.
459
472
  # @return [CArray]
460
473
  def variancep(axis: nil)
461
- return axis_variance_family(axis, :variancep) if axis
474
+ return axis_by_masked_copy(axis, :variancep) if axis
462
475
  m = moments
463
- return per_category(CA_FLOAT64) { |s| s.variancep } unless m
476
+ return per_category(core_reduce_type(:variancep)) { |s| s.variancep } unless m
464
477
  cnt = m[:count]
465
478
  vp = variance * (cnt - 1).float64 / cnt.float64
466
479
  vp[cnt.eq(0)] = UNDEF # empty / all-masked stays masked
@@ -475,9 +488,9 @@ class CACategoricalIterator < CAIterator
475
488
  # @param axis [Integer]
476
489
  # @return [CArray]
477
490
  def stddevp(axis: nil)
478
- return axis_variance_family(axis, :stddevp) if axis
491
+ return axis_by_masked_copy(axis, :stddevp) if axis
479
492
  m = moments
480
- return per_category(CA_FLOAT64) { |s| s.stddevp } unless m
493
+ return per_category(core_reduce_type(:stddevp)) { |s| s.stddevp } unless m
481
494
  variancep.sqrt
482
495
  end
483
496
 
@@ -660,8 +673,8 @@ class CACategoricalIterator < CAIterator
660
673
  # Excluded (out-of-vocabulary / masked-code) and source-masked cells join no
661
674
  # running total and are UNDEF. Mirroring the reductions (sum / mean), a scan
662
675
  # takes no axis argument. cumsum / cumprod -> float64, cummax / cummin
663
- # preserve the value dtype, cumcount -> int64 (1-based within-category
664
- # ordinal); an object value dtype is carried by the kernel's object branch.
676
+ # preserve the value data type, cumcount -> int64 (1-based within-category
677
+ # ordinal); an object value data type is carried by the kernel's object branch.
665
678
 
666
679
  # @overload cumsum
667
680
  # Per-category inclusive running sum (float64), source-shaped.
@@ -670,10 +683,10 @@ class CACategoricalIterator < CAIterator
670
683
  # Per-category inclusive running product (float64), source-shaped.
671
684
  # @return [CArray]
672
685
  # @overload cummax
673
- # Per-category inclusive running maximum (value dtype), source-shaped.
686
+ # Per-category inclusive running maximum (value data type), source-shaped.
674
687
  # @return [CArray]
675
688
  # @overload cummin
676
- # Per-category inclusive running minimum (value dtype), source-shaped.
689
+ # Per-category inclusive running minimum (value data type), source-shaped.
677
690
  # @return [CArray]
678
691
  # @overload cumcount
679
692
  # Per-category 1-based within-category ordinal (int64), source-shaped.
@@ -688,7 +701,7 @@ class CACategoricalIterator < CAIterator
688
701
  # the fused per-fiber scatter-reduce C kernel and cached (matches the flat
689
702
  # #moments caching in spirit: pay one kernel per {iterator, axis} pair, share
690
703
  # across sum / mean / min / max / minmax / count* consumers). Returns
691
- # `{count: <int64>, sum: <float64>, min: <h dtype masked>, max: <h dtype masked>}`,
704
+ # `{count: <int64>, sum: <float64>, min: <h's type, masked>, max: <h's type, masked>}`,
692
705
  # all shape [K, ...band].
693
706
  def axis_moments (axis)
694
707
  @axis_moments_cache ||= {}
@@ -712,13 +725,10 @@ class CACategoricalIterator < CAIterator
712
725
  @axis_moments_cache[axis] = {count: counts, sum: sums, min: mins, max: maxs}
713
726
  end
714
727
 
715
- # Axis-aware sum: from moments, cast float64 sums to h dtype so empty-group
716
- # identity 0 rides (matching flat #sum).
728
+ # Axis-aware sum: the moments sum is already the core fold in the core's own
729
+ # type, so it is handed back as is (an empty group cell carries identity 0.0).
717
730
  def axis_sum (axis)
718
- m = axis_moments(axis)
719
- out = CArray.new(@value.data_type, m[:sum].shape)
720
- out[] = m[:sum]
721
- out
731
+ axis_moments(axis)[:sum].copy
722
732
  end
723
733
 
724
734
  # Axis-aware mean: sums / counts (float64); empty group cells (count=0) MASKED.
@@ -731,17 +741,19 @@ class CACategoricalIterator < CAIterator
731
741
  out
732
742
  end
733
743
 
734
- # Axis-aware variance / stddev / variancep / stddevp — Ruby-level per-c mask
735
- # then delegate to the source's own axis-aware kernel. Order (median /
736
- # percentile / quantile) is genuinely order-statistical (needs sort per
737
- # group), and remains deferred; the variance family is only a centred
738
- # two-pass numeric aggregate, so this loop hits the same ε-close two-pass
739
- # kernel per (group, axis) that CArray#variance uses, no new C needed.
744
+ # Axis-aware reduction by masked copy — Ruby-level per-c mask, then delegate
745
+ # to the source's own axis-aware kernel, so the core contract for `op` rides
746
+ # unchanged. Used by the variance family (a centred two-pass numeric
747
+ # aggregate, hitting the same ε-close kernel per (group, axis) that
748
+ # CArray#variance uses) and by `accumulate` (whose in-type wrapping fold has
749
+ # no float64 moment to read it off). Order (median / percentile / quantile)
750
+ # is genuinely order-statistical (needs a sort per group) and remains
751
+ # deferred.
740
752
  #
741
753
  # Cost: K axis-reductions over an h-shaped local (most cells masked away for
742
754
  # each c) — bounded by K, typically small. A fused per-fiber variance
743
755
  # kernel is a natural follow-on if bench demands it.
744
- def axis_variance_family (axis, op)
756
+ def axis_by_masked_copy (axis, op, out_data_type = CA_FLOAT64)
745
757
  h = @value
746
758
  unless axis.is_a?(Integer) && axis >= 0 && axis < h.ndim
747
759
  raise ArgumentError,
@@ -750,7 +762,7 @@ class CACategoricalIterator < CAIterator
750
762
  end
751
763
  full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
752
764
  band = h.shape.dup; band.delete_at(axis)
753
- out = CArray.float64(*([@k] + band))
765
+ out = CArray.new(out_data_type, [@k] + band)
754
766
  slot_idx = [nil] + [nil] * band.size # placeholder; c fills slot 0
755
767
  codes_bad = full_c.has_mask? ? full_c.is_masked : nil
756
768
  @k.times do |c|
@@ -855,9 +867,8 @@ class CACategoricalIterator < CAIterator
855
867
  # Called from median / percentile / variance / stddev when axis: is given.
856
868
  def axis_order_stat_defer! (op)
857
869
  raise NotImplementedError,
858
- "CACategoricalIterator##{op}(axis:) not yet implemented — order " \
859
- "statistics deferred to Phase 4 of PROPOSAL_CATEGORICAL_REDUCE_AXIS " \
860
- "(needs per-fiber counting-sort C kernel, tracked separately)."
870
+ "CACategoricalIterator##{op}(axis:) is not implemented — order " \
871
+ "statistics are available without axis:."
861
872
  end
862
873
 
863
874
  # Drive a segment scan through the axis-group scan kernel: the whole value as
@@ -910,7 +921,7 @@ class CACategoricalIterator < CAIterator
910
921
 
911
922
  # Fused per-segment weighted sum + weighted mean (one C pass over the grouped
912
923
  # copy, weights in group order). Returns [wsum, wmean]; wmean is masked where a
913
- # segment has no present (value AND weight) pair. Numeric value dtypes only.
924
+ # segment has no present (value AND weight) pair. Numeric value data types only.
914
925
  def kernel_weighted (wg)
915
926
  ws = CArray.float64(@k)
916
927
  wm = CArray.float64(@k)
@@ -918,7 +929,7 @@ class CACategoricalIterator < CAIterator
918
929
  [ws, wm]
919
930
  end
920
931
 
921
- # Per-group weighted fallback for non-numeric value dtypes (complex): delegate
932
+ # Per-group weighted fallback for non-numeric value data types (complex): delegate
922
933
  # each group to CArray#wsum / #wmean. Empty segments take the given identity.
923
934
  def fold_weighted (wg, empty)
924
935
  out = CArray.float64(@k)
@@ -943,9 +954,9 @@ class CACategoricalIterator < CAIterator
943
954
  # Single-pass reduceat moments (count / sum / min / max per category), computed
944
955
  # once over the grouped copy and cached — the whole point of the eager copy is
945
956
  # that one scatter is followed by cheap single-pass reductions with no
946
- # per-segment views. Nil for a non-numeric value dtype (complex / object /
957
+ # per-segment views. Nil for a non-numeric value data type (complex / object /
947
958
  # bool), where the monoid reductions fall back to per_category.
948
- # numeric value dtypes the C moments kernel handles (int8..float64); bool /
959
+ # numeric value data types the C moments kernel handles (int8..float64); bool /
949
960
  # complex / object fall back to per_category.
950
961
  MONOID_TYPES = %i[int8 uint8 int16 uint16 int32 uint32
951
962
  int64 uint64 float32 float64].freeze
@@ -965,7 +976,7 @@ class CACategoricalIterator < CAIterator
965
976
  end
966
977
 
967
978
  # Single-pass fused group-local argmin / argmax (min_index / max_index),
968
- # cached. Nil for a non-numeric value dtype (fall back to per_category).
979
+ # cached. Nil for a non-numeric value data type (fall back to per_category).
969
980
  def arg_minmax
970
981
  return @arg_minmax if defined?(@arg_minmax)
971
982
  @arg_minmax =
@@ -978,7 +989,7 @@ class CACategoricalIterator < CAIterator
978
989
  end
979
990
 
980
991
  # Single-pass fused per-category boolean all / any, cached. Nil unless the
981
- # value dtype is boolean (fall back to per_category, which raises like
992
+ # value data type is boolean (fall back to per_category, which raises like
982
993
  # CArray#all on a non-boolean).
983
994
  def all_any
984
995
  return @all_any if defined?(@all_any)
@@ -993,11 +1004,22 @@ class CACategoricalIterator < CAIterator
993
1004
 
994
1005
  # Build a length-k typed output by folding each category's members with the
995
1006
  # given reduction block. Fallback path (order statistics, and monoids on a
996
- # non-numeric value dtype): each group is delegated to the same CArray
1007
+ # non-numeric value data type): each group is delegated to the same CArray
997
1008
  # reduction, so the per-group result matches `CArray#<reduction>` over that
998
1009
  # group's members — the mask carries the "insufficient present data" contract
999
1010
  # for free (an all-masked group reduces like an empty one; identity-bearing
1000
1011
  # reductions return their identity, ratios return UNDEF; see ext ERI).
1012
+ # The data type the core reduction `op` promotes this value to. Asked of the
1013
+ # core itself -- a one-cell reduction of the value's type -- rather than
1014
+ # restated here, so a per-category answer cannot drift from `CArray#<op>`
1015
+ # (`sum` on an integer promotes, `accumulate` stays, `min` / `max` keep the
1016
+ # type but a boolean widens, `prod` on an object stays an object). A payload
1017
+ # the core refuses to fold this way raises here, with the core's own error.
1018
+ def core_reduce_type (op, *args)
1019
+ (@core_reduce_type ||= {})[[op, args]] ||=
1020
+ CArray.new(@grouped.data_type, [1, 1]).public_send(op, *args, axis: 1).data_type
1021
+ end
1022
+
1001
1023
  def per_category (data_type)
1002
1024
  out = CArray.new(data_type, [@k])
1003
1025
  @k.times { |c| out[c] = yield(group_slice(c)) }
@@ -81,7 +81,7 @@ class CArray
81
81
  result
82
82
  end
83
83
 
84
- # @overload conditional(cond, then_fn, else_fn, dtype: nil)
84
+ # @overload conditional(cond, then_fn, else_fn, data_type: nil)
85
85
  # Returns per-cell `then_fn.call(self[cond])` where `cond` is
86
86
  # true and `else_fn.call(self[cond.not])` where it is false.
87
87
  # The two callables are applied only to their own subset of
@@ -91,14 +91,14 @@ class CArray
91
91
  # Scalar returns from a callable (e.g. `->(v) { 0 }`) broadcast
92
92
  # to the subset shape. The result `data_type` is the promotion
93
93
  # of the two subset results via `CArray.result_type`, or
94
- # `dtype` when given. Masked cells in `cond` propagate to
94
+ # `data_type` when given. Masked cells in `cond` propagate to
95
95
  # `UNDEF` in the result.
96
96
  #
97
97
  # @param cond [CArray] boolean selector; same shape as `self`.
98
98
  # @param then_fn [#call] callable applied to `self[cond]`.
99
99
  # @param else_fn [#call] callable applied to `self[cond.not]`.
100
- # @param dtype [Symbol, Integer, nil] override for the result
101
- # `data_type`.
100
+ # @param data_type [Symbol, Integer, nil] override for the result
101
+ # data type.
102
102
  # @return [CArray] new array with the same shape as `self`.
103
103
  # @raise [ArgumentError] when `cond` is not a same-shape
104
104
  # boolean CArray.
@@ -108,7 +108,7 @@ class CArray
108
108
  # ->(v) { v.log }, # domain-safe: only positive cells
109
109
  # ->(v) { -v })
110
110
  # # => [2.0, 1.0, -0.0, 0.0, 0.6931..., 1.0986...]
111
- def conditional (cond, then_fn, else_fn, dtype: nil)
111
+ def conditional (cond, then_fn, else_fn, data_type: nil)
112
112
  unless cond.is_a?(CArray) && cond.boolean? && cond.shape == self.shape
113
113
  raise ArgumentError,
114
114
  "conditional: cond must be a boolean CArray with same shape as self"
@@ -123,13 +123,13 @@ class CArray
123
123
  # to the subset shape; wrap it here so the scatter step below sees a
124
124
  # same-length CArray.
125
125
  unless y_then.is_a?(CArray)
126
- y_then = CArray.new(dtype || CArray.result_type(y_then), x_then.shape).fill(y_then)
126
+ y_then = CArray.new(data_type || CArray.result_type(y_then), x_then.shape).fill(y_then)
127
127
  end
128
128
  unless y_else.is_a?(CArray)
129
- y_else = CArray.new(dtype || CArray.result_type(y_else), x_else.shape).fill(y_else)
129
+ y_else = CArray.new(data_type || CArray.result_type(y_else), x_else.shape).fill(y_else)
130
130
  end
131
131
 
132
- dt = dtype || CArray.result_type(y_then, y_else)
132
+ dt = data_type || CArray.result_type(y_then, y_else)
133
133
  out = CArray.new(dt, self.shape)
134
134
  out[cond] = y_then
135
135
  out[cond.not] = y_else
@@ -141,7 +141,7 @@ class CArray
141
141
  out
142
142
  end
143
143
 
144
- # @overload select(condlist, choicelist, default: 0, dtype: nil)
144
+ # @overload select(condlist, choicelist, default: 0, data_type: nil)
145
145
  # Multi-way ternary select: for each cell, picks the value from
146
146
  # the first `choicelist[k]` whose matching `condlist[k]` is true,
147
147
  # falling back to `default` when no condition holds. When several
@@ -151,15 +151,15 @@ class CArray
151
151
  # Each `choicelist[k]` is either a same-shape CArray or a scalar
152
152
  # broadcast to every cell. The result `data_type` is the promotion
153
153
  # of every choice plus `default` via `CArray.result_type`, or
154
- # `dtype` when given.
154
+ # `data_type` when given.
155
155
  #
156
156
  # @param condlist [Array<CArray>] boolean selectors.
157
157
  # @param choicelist [Array<CArray, Numeric, Object>] values, one
158
158
  # per condition (same length as `condlist`).
159
159
  # @param default [CArray, Numeric, Object] value written where no
160
160
  # condition holds.
161
- # @param dtype [Symbol, Integer, nil] override for the result
162
- # `data_type`.
161
+ # @param data_type [Symbol, Integer, nil] override for the result
162
+ # data type.
163
163
  # @return [CArray] new array with the shape of `condlist[0]`.
164
164
  # @raise [ArgumentError] on size mismatch, empty `condlist`, or a
165
165
  # non-boolean / wrong-shape entry in `condlist`.
@@ -169,7 +169,7 @@ class CArray
169
169
  # [-x, x * 10],
170
170
  # default: 999)
171
171
  # # => [5.0, 3.0, -10.0, 0.0, 10.0, 999.0]
172
- def self.select (condlist, choicelist, default: 0, dtype: nil)
172
+ def self.select (condlist, choicelist, default: 0, data_type: nil)
173
173
  unless condlist.is_a?(Array) && choicelist.is_a?(Array)
174
174
  raise ArgumentError, "select: condlist and choicelist must be Arrays"
175
175
  end
@@ -187,7 +187,7 @@ class CArray
187
187
  end
188
188
  shape = first.shape
189
189
 
190
- dt = dtype || CArray.result_type(*choicelist, default)
190
+ dt = data_type || CArray.result_type(*choicelist, default)
191
191
  # `default` can be either a same-shape CArray (per-cell fallback) or a
192
192
  # scalar (broadcast to every cell).
193
193
  default_full = default.is_a?(CArray) && !default.scalar?