carray 3.0.2 → 3.0.4

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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +568 -0
  3. data/README.md +6 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +220 -0
  7. data/ext/ca_axis_group.c +201 -123
  8. data/ext/ca_binop_dispatch.c +46 -52
  9. data/ext/ca_binop_dispatch.h +1 -1
  10. data/ext/ca_categorical_iterator.c +249 -183
  11. data/ext/ca_compare.c +7 -12
  12. data/ext/ca_for_buffer.h +21 -68
  13. data/ext/ca_group_iter.c +38 -23
  14. data/ext/ca_kernel_iterator.c +1765 -2076
  15. data/ext/ca_kernel_iterator.h +584 -566
  16. data/ext/ca_monop_dispatch.c +86 -63
  17. data/ext/ca_monop_dispatch.h +5 -0
  18. data/ext/ca_obj_array.c +223 -56
  19. data/ext/ca_obj_bincmp.c +172 -138
  20. data/ext/ca_obj_binop.c +116 -230
  21. data/ext/ca_obj_bitarray.c +40 -26
  22. data/ext/ca_obj_bitfield.c +100 -68
  23. data/ext/ca_obj_block.c +19 -9
  24. data/ext/ca_obj_byte_swap.c +45 -46
  25. data/ext/ca_obj_const_string.c +269 -44
  26. data/ext/ca_obj_face.c +612 -89
  27. data/ext/ca_obj_face.h +111 -47
  28. data/ext/ca_obj_fake.c +36 -79
  29. data/ext/ca_obj_farray.c +7 -3
  30. data/ext/ca_obj_field.c +33 -8
  31. data/ext/ca_obj_fixlen_string.c +23 -24
  32. data/ext/ca_obj_grid.c +541 -210
  33. data/ext/ca_obj_meld.c +331 -111
  34. data/ext/ca_obj_moncmp.c +80 -65
  35. data/ext/ca_obj_monop.c +177 -149
  36. data/ext/ca_obj_object.c +664 -248
  37. data/ext/ca_obj_record.c +63 -23
  38. data/ext/ca_obj_reduce.c +18 -16
  39. data/ext/ca_obj_refer.c +140 -58
  40. data/ext/ca_obj_remap.c +85 -24
  41. data/ext/ca_obj_repeat.c +24 -9
  42. data/ext/ca_obj_roll.c +121 -16
  43. data/ext/ca_obj_select.c +61 -58
  44. data/ext/ca_obj_select_axis.c +73 -107
  45. data/ext/ca_obj_shift.c +32 -102
  46. data/ext/ca_obj_stack.c +372 -122
  47. data/ext/ca_obj_stride.c +348 -140
  48. data/ext/ca_obj_string.c +88 -22
  49. data/ext/ca_obj_tile.c +38 -21
  50. data/ext/ca_obj_time.c +45 -31
  51. data/ext/ca_obj_timedelta.c +43 -29
  52. data/ext/ca_obj_transpose.c +17 -6
  53. data/ext/ca_obj_triop.c +125 -141
  54. data/ext/ca_obj_window.c +330 -44
  55. data/ext/ca_op_ipower.c +24 -11
  56. data/ext/ca_op_powi.h +57 -30
  57. data/ext/ca_pow5_table.h +656 -0
  58. data/ext/ca_sort_kernels.h +70 -0
  59. data/ext/ca_sweep_engine.c +104 -81
  60. data/ext/ca_sweep_engine.h +7 -20
  61. data/ext/ca_triop_dispatch.c +4 -1
  62. data/ext/ca_triop_dispatch.h +5 -2
  63. data/ext/caframe_csv_reader.c +542 -0
  64. data/ext/carray.h +241 -92
  65. data/ext/carray_access.c +608 -276
  66. data/ext/carray_address_basis.c +494 -97
  67. data/ext/carray_attribute.c +129 -4
  68. data/ext/carray_bincount.c +39 -7
  69. data/ext/carray_broadcast.c +56 -19
  70. data/ext/carray_build_flags.h +1 -1
  71. data/ext/carray_call_cfunc.c +108 -28
  72. data/ext/carray_cast.c +1576 -223
  73. data/ext/carray_cast_func.rb +77 -24
  74. data/ext/carray_class.c +4 -3
  75. data/ext/carray_conversion.c +208 -75
  76. data/ext/carray_copy.c +82 -24
  77. data/ext/carray_core.c +646 -162
  78. data/ext/carray_count.c +171 -88
  79. data/ext/carray_element.c +94 -60
  80. data/ext/carray_factorize.c +670 -324
  81. data/ext/carray_generate.c +260 -83
  82. data/ext/carray_histogram.c +209 -106
  83. data/ext/carray_hold.c +14 -16
  84. data/ext/carray_index_classifier.c +422 -243
  85. data/ext/carray_internal.h +692 -3
  86. data/ext/carray_kernels_bincmp.c +7 -0
  87. data/ext/carray_kernels_binop.c +487 -120
  88. data/ext/carray_kernels_init.c +653 -50
  89. data/ext/carray_kernels_moncmp.c +15 -8
  90. data/ext/carray_kernels_monop.c +99 -43
  91. data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
  92. data/ext/carray_kernels_reduce_boolean.c +112 -36
  93. data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
  94. data/ext/carray_kernels_reduce_extreme.c +3032 -2892
  95. data/ext/carray_kernels_reduce_variance.c +439 -500
  96. data/ext/carray_kernels_scan.c +1097 -488
  97. data/ext/carray_kernels_search.c +3787 -2176
  98. data/ext/carray_kernels_sort.c +1200 -920
  99. data/ext/carray_kernels_triop.c +479 -7
  100. data/ext/carray_key_windows.c +216 -0
  101. data/ext/carray_lazy.c +967 -153
  102. data/ext/carray_loop.c +81 -65
  103. data/ext/carray_mask.c +910 -324
  104. data/ext/carray_math_kernel.h +1 -0
  105. data/ext/carray_median_percentile.c +299 -100
  106. data/ext/carray_memory_view.c +872 -758
  107. data/ext/carray_operator.c +1171 -364
  108. data/ext/carray_order.c +505 -133
  109. data/ext/carray_partition.c +112 -65
  110. data/ext/carray_random.c +327 -239
  111. data/ext/carray_round.c +273 -0
  112. data/ext/carray_scatter.c +346 -78
  113. data/ext/carray_segment.c +257 -0
  114. data/ext/carray_slab.c +249 -86
  115. data/ext/carray_sort.c +224 -227
  116. data/ext/carray_sort_kernel.c +8 -1
  117. data/ext/carray_struct.c +118 -39
  118. data/ext/carray_test.c +193 -69
  119. data/ext/carray_time_text.c +1182 -0
  120. data/ext/carray_undef.c +4 -0
  121. data/ext/carray_utils.c +579 -113
  122. data/ext/extconf.rb +27 -2
  123. data/ext/mk_call_cfunc.rb +19 -4
  124. data/ext/mk_pow5_table.rb +35 -0
  125. data/ext/mkkernel.rb +1202 -1014
  126. data/ext/ruby_carray.c +17 -5
  127. data/ext/version.h +4 -4
  128. data/ext/version.rb +4 -2
  129. data/lib/carray/attribute.rb +160 -17
  130. data/lib/carray/autoload_carray.rb +23 -27
  131. data/lib/carray/autoload_method_extension.rb +20 -5
  132. data/lib/carray/axis_group.rb +360 -63
  133. data/lib/carray/basics.rb +162 -28
  134. data/lib/carray/bincount_nd.rb +18 -2
  135. data/lib/carray/block_iterator.rb +231 -58
  136. data/lib/carray/boolean_reduce.rb +34 -42
  137. data/lib/carray/categorical.rb +242 -52
  138. data/lib/carray/categorical_iterator.rb +437 -827
  139. data/lib/carray/complex.rb +41 -34
  140. data/lib/carray/conditional.rb +150 -14
  141. data/lib/carray/const_string.rb +59 -4
  142. data/lib/carray/construct.rb +15 -11
  143. data/lib/carray/data_type_extension.rb +1 -0
  144. data/lib/carray/distance_limit.rb +77 -0
  145. data/lib/carray/fixlen_string.rb +7 -1
  146. data/lib/carray/frame/column_verbs.rb +70 -0
  147. data/lib/carray/frame/concat.rb +157 -6
  148. data/lib/carray/frame/convert.rb +82 -7
  149. data/lib/carray/frame/csv_parser.rb +688 -87
  150. data/lib/carray/frame/describe.rb +80 -0
  151. data/lib/carray/frame/frame.rb +369 -45
  152. data/lib/carray/frame/group.rb +631 -90
  153. data/lib/carray/frame/io.rb +506 -79
  154. data/lib/carray/frame/join.rb +411 -62
  155. data/lib/carray/frame/records.rb +116 -19
  156. data/lib/carray/frame/reshape.rb +455 -0
  157. data/lib/carray/frame/sort.rb +30 -18
  158. data/lib/carray/frame/verbs.rb +868 -61
  159. data/lib/carray/frame/windows.rb +117 -0
  160. data/lib/carray/frame.rb +4 -0
  161. data/lib/carray/fuse_source.rb +118 -2
  162. data/lib/carray/fusion.rb +257 -42
  163. data/lib/carray/histogram.rb +6 -1
  164. data/lib/carray/inspect.rb +54 -13
  165. data/lib/carray/iterator.rb +99 -9
  166. data/lib/carray/key_windows.rb +620 -0
  167. data/lib/carray/lazy.rb +216 -193
  168. data/lib/carray/mask_gap_fill.rb +112 -92
  169. data/lib/carray/math.rb +32 -19
  170. data/lib/carray/meld_reduce.rb +52 -21
  171. data/lib/carray/methods/align_addr.rb +8 -4
  172. data/lib/carray/methods/bin.rb +26 -18
  173. data/lib/carray/methods/bincount.rb +18 -2
  174. data/lib/carray/methods/bit_string.rb +15 -4
  175. data/lib/carray/methods/choose.rb +16 -3
  176. data/lib/carray/methods/composition.rb +18 -10
  177. data/lib/carray/methods/factorize.rb +2 -2
  178. data/lib/carray/methods/gather_nd.rb +25 -5
  179. data/lib/carray/methods/insert_block.rb +9 -14
  180. data/lib/carray/methods/is_between.rb +59 -0
  181. data/lib/carray/methods/is_in.rb +140 -10
  182. data/lib/carray/methods/join.rb +2 -7
  183. data/lib/carray/methods/lag.rb +49 -0
  184. data/lib/carray/methods/locate_addr.rb +182 -45
  185. data/lib/carray/methods/mode.rb +18 -1
  186. data/lib/carray/methods/nunique.rb +11 -6
  187. data/lib/carray/methods/repeat.rb +1 -20
  188. data/lib/carray/methods/resize.rb +21 -23
  189. data/lib/carray/methods/snap.rb +34 -12
  190. data/lib/carray/methods/unique.rb +11 -2
  191. data/lib/carray/methods/value_counts.rb +4 -11
  192. data/lib/carray/record_order.rb +208 -0
  193. data/lib/carray/rng.rb +3 -0
  194. data/lib/carray/runtime.rb +65 -17
  195. data/lib/carray/segment_iterator.rb +1221 -0
  196. data/lib/carray/serialize.rb +214 -30
  197. data/lib/carray/slab_iterator.rb +101 -27
  198. data/lib/carray/stack.rb +10 -4
  199. data/lib/carray/string.rb +105 -5
  200. data/lib/carray/string_operation_extension.rb +12 -4
  201. data/lib/carray/struct.rb +137 -58
  202. data/lib/carray/struct_builder.rb +124 -17
  203. data/lib/carray/time.rb +1222 -186
  204. data/lib/carray/window_iterator.rb +242 -81
  205. data/lib/carray.rb +9 -2
  206. data/yard-stubs/ca_obj_array.rb +4 -4
  207. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  208. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  209. data/yard-stubs/ca_obj_fake.rb +1 -1
  210. data/yard-stubs/ca_obj_field.rb +5 -1
  211. data/yard-stubs/ca_obj_grid.rb +21 -10
  212. data/yard-stubs/ca_obj_refer.rb +5 -4
  213. data/yard-stubs/ca_obj_roll.rb +2 -3
  214. data/yard-stubs/ca_obj_shift.rb +5 -1
  215. data/yard-stubs/ca_obj_stride.rb +10 -7
  216. data/yard-stubs/ca_obj_tile.rb +1 -1
  217. data/yard-stubs/ca_obj_transpose.rb +1 -1
  218. data/yard-stubs/ca_obj_window.rb +9 -5
  219. data/yard-stubs/carray_access.rb +11 -5
  220. data/yard-stubs/carray_attribute.rb +0 -5
  221. data/yard-stubs/carray_broadcast.rb +1 -1
  222. data/yard-stubs/carray_cast.rb +88 -43
  223. data/yard-stubs/carray_conversion.rb +15 -2
  224. data/yard-stubs/carray_copy.rb +12 -5
  225. data/yard-stubs/carray_core.rb +3 -3
  226. data/yard-stubs/carray_count.rb +9 -0
  227. data/yard-stubs/carray_element.rb +10 -4
  228. data/yard-stubs/carray_generate.rb +21 -1
  229. data/yard-stubs/carray_lazy.rb +5 -1
  230. data/yard-stubs/carray_loop.rb +4 -1
  231. data/yard-stubs/carray_mask.rb +60 -12
  232. data/yard-stubs/carray_math.rb +52 -14
  233. data/yard-stubs/carray_median_percentile.rb +15 -8
  234. data/yard-stubs/carray_memory_view.rb +40 -23
  235. data/yard-stubs/carray_order.rb +61 -29
  236. data/yard-stubs/carray_random.rb +13 -4
  237. data/yard-stubs/carray_scatter.rb +21 -9
  238. data/yard-stubs/carray_segment.rb +72 -0
  239. data/yard-stubs/carray_slab.rb +10 -2
  240. data/yard-stubs/carray_sort.rb +49 -8
  241. data/yard-stubs/carray_test.rb +15 -3
  242. data/yard-stubs/carray_utils.rb +0 -26
  243. metadata +19 -5
  244. data/ext/ca_for_each_element.h +0 -239
  245. data/ext/carray_kernels_map.c +0 -3467
  246. data/lib/carray/arrow_tensor.rb +0 -401
  247. data/lib/carray/table.rb +0 -108
@@ -213,6 +213,25 @@ class AxisGroup
213
213
  end
214
214
  end
215
215
 
216
+ # The keywords a group reduction takes besides axis:. Checked before the
217
+ # work, so an unknown keyword is refused rather than dropped.
218
+ REDUCE_KEYWORDS = [:axis, :min_count, :fill_value].freeze
219
+
220
+ # all / any take axis: only, as the core ones do.
221
+ LOGICAL_REDUCE_KEYWORDS = [:axis].freeze
222
+
223
+ # A boolean array carrying value's mask: what a count reads, for a value
224
+ # whose payload the group kernel cannot read. Called from C.
225
+ private_class_method def self.count_stand_in (value)
226
+ return value if value.boolean? || value.integer? || value.float?
227
+ stand_in = CArray.boolean(*value.shape)
228
+ stand_in.mask = value.mask if value.has_mask?
229
+ stand_in
230
+ end
231
+
232
+ # A count of the mask reads every cell of the group, as the core's does.
233
+ MASK_COUNTS = [:count, :count_not_masked, :count_masked].freeze
234
+
216
235
  # Parse an axis: spec into [has_group, fused_band_slot_positions].
217
236
  def self.parse_axis (axis)
218
237
  has_group = false
@@ -332,58 +351,109 @@ class CAGroupIterator
332
351
  # -- arrived as "wrong number of arguments", which reads as a method that
333
352
  # does not take one rather than one whose engine has no such fold. Both are
334
353
  # composed here instead, out of folds the engine does have.
335
- alias __count_present__ count
336
-
337
- # @overload count
338
- # Per-group count of present (non-masked) cells.
339
- # @overload count(UNDEF)
340
- # Per-group count of masked cells, as {#count_masked}.
341
- # @overload count(v)
342
- # Per-group count of cells equal to `v`.
343
- # @return [CArray]
344
- def count (*args, **kw)
345
- return __count_present__(**kw) if args.empty?
346
- if args.size > 1
347
- raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
354
+ # They are in a module in front of the engine's own count, all and any,
355
+ # which they reach with super; so are the reductions below that choose
356
+ # between the engine and the core.
357
+ module ComposedFolds
358
+
359
+ # @overload count
360
+ # Per-group count of present (non-masked) cells.
361
+ # @overload count(UNDEF)
362
+ # Per-group count of masked cells, as {#count_masked}.
363
+ # @overload count(v)
364
+ # Per-group count of cells equal to `v`.
365
+ # @return [CArray]
366
+ def count (*args, **kw)
367
+ if args.empty?
368
+ return core_counts?(kw) ? reduce_by_core(:count, kw) : super(**kw)
369
+ end
370
+ if args.size > 1
371
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)"
372
+ end
373
+ v = args.first
374
+ return count_masked(**kw) if v.equal?(UNDEF)
375
+ # eq leaves a masked cell UNDEF, so the sum leaves it out and its
376
+ # min_count: counts the present cells, as the core count(v) does
377
+ regroup(value.eq(v).int64, spec).sum(**kw).int64
348
378
  end
349
- v = args.first
350
- return count_masked(**kw) if v.equal?(UNDEF)
351
- # a masked cell equals nothing, and eq marks it UNDEF; drop that to false
352
- # so it is simply not counted
353
- hit = value.eq(v)
354
- hit = hit.strip_mask(false) if hit.has_mask?
355
- self.class.__build__(hit.int64, spec).sum(**kw).int64
356
- end
357
379
 
358
- alias __all_folded__ all
359
- alias __any_folded__ any
380
+ # @overload all
381
+ # Per-group `all` over a boolean payload.
382
+ # @return [CArray]
383
+ def all (**kw)
384
+ boolean_payload!(:all)
385
+ core_counts?(kw) ? reduce_by_core(:all, kw) : super(**kw)
386
+ end
360
387
 
361
- # @overload all
362
- # Per-group `all` over a boolean payload.
363
- # @return [CArray]
364
- def all (**kw)
365
- boolean_payload!(:all)
366
- __all_folded__(**kw)
367
- end
388
+ # @overload any
389
+ # Per-group `any` over a boolean payload.
390
+ # @return [CArray]
391
+ def any (**kw)
392
+ boolean_payload!(:any)
393
+ core_counts?(kw) ? reduce_by_core(:any, kw) : super(**kw)
394
+ end
368
395
 
369
- # @overload any
370
- # Per-group `any` over a boolean payload.
371
- # @return [CArray]
372
- def any (**kw)
373
- boolean_payload!(:any)
374
- __any_folded__(**kw)
396
+ # @overload count_not_masked
397
+ # Per-group count of present (non-masked) cells.
398
+ # @return [CArray]
399
+ def count_not_masked (**kw)
400
+ core_counts?(kw) ? reduce_by_core(:count_not_masked, kw) : super(**kw)
401
+ end
402
+
403
+ private
404
+
405
+ # Whether a count or all / any of each group is taken from the core, as
406
+ # the other reductions of a payload the engine does not read are. The
407
+ # engine answers these for every payload; with the fast paths off (a
408
+ # development build run with CARRAY_GENERAL_PATH set) the core does --
409
+ # except for a band folded into the count, which has no route but the
410
+ # engine.
411
+ def core_counts? (kw)
412
+ return false if CArray.__fast_paths_enabled__
413
+ has_group, fused = AxisGroup.parse_axis(kw[:axis])
414
+ has_group && fused.empty?
415
+ end
375
416
  end
417
+ prepend ComposedFolds
376
418
 
377
419
  # Per-group classified cell count (mask-independent) = count on the
378
420
  # mask-stripped value, so every classified cell is counted regardless of the
379
421
  # value mask (unlike count / count_not_masked, which count present cells).
380
422
  def elements (**kw)
381
- self.class.__build__(value.value, spec).count(**kw)
423
+ regroup(value.value, spec).count(**kw)
382
424
  end
383
425
 
384
426
  # Per-group count of value-masked cells = elements - count_not_masked.
385
427
  def count_masked (**kw)
386
- elements(**kw) - count_not_masked(**kw)
428
+ has_group, = AxisGroup.parse_axis(kw[:axis])
429
+ return value.count_masked(**kw) unless has_group
430
+ check_reduce_keywords(:count_masked, kw)
431
+ plain = { axis: kw[:axis] }
432
+ finish_reduction(:count_masked, kw,
433
+ elements(**plain) - count_not_masked(**plain))
434
+ end
435
+
436
+ # The scatter kernel reads an integer or float payload, and answers there
437
+ # as the core does. Any other payload -- boolean, complex, object, a Face --
438
+ # is reduced group by group by the core, through the composite categorical,
439
+ # so the data type, the Face and a refusal are the core's: a boolean's sum
440
+ # is uint64, a time column's minimum a time, its sum refused.
441
+ CArray.send(:define_named_methods, ComposedFolds,
442
+ [:sum, :accumulate, :prod, :mean, :min, :max,
443
+ :variance, :stddev, :variancep, :stddevp],
444
+ :scatter_or_core, super_block: true)
445
+
446
+ # The scatter kernel is the block (super).
447
+ private def scatter_or_core (op, args, kw)
448
+ unless args.empty?
449
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)"
450
+ end
451
+ has_group, fused = AxisGroup.parse_axis(kw[:axis])
452
+ if ! has_group || kernel_reads_value?(fused)
453
+ yield(**kw)
454
+ else
455
+ reduce_by_core(op, kw)
456
+ end
387
457
  end
388
458
 
389
459
  # Per-group [min, max] pair (matching CArray#minmax).
@@ -418,7 +488,7 @@ class CAGroupIterator
418
488
  # value|weight mask, so masked cells drop out. Empty group -> 0.0 (identity).
419
489
  # No weighted kernel needed -- it is a plain group-sum of a derived array.
420
490
  def wsum (weights, **kw)
421
- self.class.__build__(value * weights, spec).sum(**kw)
491
+ regroup(value * weights, spec).sum(**kw)
422
492
  end
423
493
 
424
494
  # Weighted mean = Sum(v*w) / Sum(w), both over the combined present-set. The
@@ -427,13 +497,21 @@ class CAGroupIterator
427
497
  # (no present pair) is UNDEF (matching CArray#wmean); a present group whose
428
498
  # weights sum to zero yields NaN/Inf (core's 0/0).
429
499
  def wmean (weights, **kw)
500
+ has_group, = AxisGroup.parse_axis(kw[:axis])
501
+ return value.wmean(weights, **kw) unless has_group
502
+ check_reduce_keywords(:wmean, kw)
503
+ plain = { axis: kw[:axis] }
430
504
  prod = value * weights
431
- num = self.class.__build__(prod, spec).sum(**kw)
432
- den = self.class.__build__(value * 0 + weights, spec).sum(**kw)
433
- cnt = self.class.__build__(prod, spec).count_not_masked(**kw)
434
- out = num / den
435
- out[cnt.eq(0)] = UNDEF # no present pair -> masked (empty group)
436
- out
505
+ num = regroup(prod, spec).sum(**plain)
506
+ den = regroup(value * 0 + weights, spec).sum(**plain)
507
+ cnt = regroup(prod, spec).count_not_masked(**plain)
508
+ # No present pair is UNDEF (an empty group), set on the denominator so
509
+ # the division never meets an empty group's 0 / 0.
510
+ den = den.copy
511
+ den[cnt.eq(0)] = UNDEF
512
+ # min_count: counts the present pairs, as the core wmean does.
513
+ pairs = regroup(prod, spec)
514
+ pairs.finish_reduction(:wmean, kw, num / den)
437
515
  end
438
516
 
439
517
  # ---- segment scan: within-group running statistics ---------------------
@@ -444,7 +522,7 @@ class CAGroupIterator
444
522
  # in row-major position order along the grouped axes (per band). Excluded /
445
523
  # source-masked cells are UNDEF (they join no running total). All five route
446
524
  # through the shared scan_op dispatcher, driving the fused C kernel
447
- # __axis_group_scan__ (ext/ca_axis_group.c) with peak O(k) extra. A scan
525
+ # __scan_by_axis_groups__ (ext/ca_axis_group.c) with peak O(k) extra. A scan
448
526
  # cannot fold a band into the statistic (that would collapse an axis), so it
449
527
  # accepts only axis: :group; without :group each delegates to the value's
450
528
  # same-named scan.
@@ -493,6 +571,59 @@ class CAGroupIterator
493
571
  scan_op(:cumcount, kw)
494
572
  end
495
573
 
574
+ # @overload lag(n = 1, axis: :group)
575
+ # Per-group value `n` steps earlier, counting a group's members in
576
+ # row-major position order along the grouped axes (per band), as the
577
+ # scans do; UNDEF where the group has none, so a lag never reaches
578
+ # another group. Without :group it is the value's own `lag` along an
579
+ # axis. A negative `n` looks ahead.
580
+ # @return [CArray] source-shaped
581
+ def lag (n = 1, axis: nil)
582
+ has_group, fused = AxisGroup.parse_axis(axis)
583
+ return value.lag(n, axis: axis) unless has_group
584
+ unless fused.empty?
585
+ raise ArgumentError,
586
+ "axis_group lag: folding a band into a lag (axis: [:group, k]) " \
587
+ "is not supported; a lag preserves the source shape"
588
+ end
589
+ scan_by_core(:lag, n)
590
+ end
591
+
592
+ # @overload strip_mask(method:, axis: :group)
593
+ # Per-group fill of the masked cells, counting a group's members in
594
+ # row-major position order along the grouped axes (per band), as the
595
+ # scans do: `:forward` / `:ffill` or `:backward` / `:bfill`, never across
596
+ # into another group. Without :group it is the value's own
597
+ # `strip_mask(method:, axis:)`.
598
+ # @return [CArray] source-shaped
599
+ def strip_mask (method:, axis: nil)
600
+ has_group, fused = AxisGroup.parse_axis(axis)
601
+ return value.strip_mask(method: method, axis: axis) unless has_group
602
+ CAIterator.hold_backward?(method)
603
+ unless fused.empty?
604
+ raise ArgumentError,
605
+ "axis_group strip_mask: folding a band into a fill (axis: [:group, k]) " \
606
+ "is not supported; a fill preserves the source shape"
607
+ end
608
+ scan_by_core(:strip_mask, method: method)
609
+ end
610
+
611
+ # @overload order(axis: :group, descending: false, method: :ordinal, kind: :quick)
612
+ # Per-group rank of each cell among its group's members (int64, `0` the
613
+ # smallest; per band), as `CArray#order` ranks them; a cell in no group is
614
+ # UNDEF. Without :group it is the value's own `order`.
615
+ # @return [CArray] source-shaped
616
+ def order (axis: nil, **kw)
617
+ has_group, fused = AxisGroup.parse_axis(axis)
618
+ return value.order(axis: axis, **kw) unless has_group
619
+ unless fused.empty?
620
+ raise ArgumentError,
621
+ "axis_group order: folding a band into an order (axis: [:group, k]) " \
622
+ "is not supported; an order preserves the source shape"
623
+ end
624
+ scan_by_core(:order, **kw)
625
+ end
626
+
496
627
  # ---- materialize path: order statistics + generic iteration ------------
497
628
  #
498
629
  # These need every member of a group held together, so (unlike the scatter
@@ -534,6 +665,41 @@ class CAGroupIterator
534
665
  order_stat(:quantile, [], kw)
535
666
  end
536
667
 
668
+ # @overload first(axis: :group)
669
+ # Per-group first present value, in row-major position order along the
670
+ # grouped axes (per band); UNDEF for a group with none. Without :group it
671
+ # is the value's own `first`.
672
+ # @return [CArray]
673
+ # @overload last(axis: :group)
674
+ # Per-group last present value; UNDEF for a group with none.
675
+ # @return [CArray]
676
+ # @overload nunique(axis: :group)
677
+ # Per-group number of distinct present values (int64); 0 for a group with
678
+ # none.
679
+ # @return [CArray]
680
+ #
681
+ # These take no `min_count:` or `fill_value:`, as the core ones do not. A
682
+ # group is gathered whole, so folding a band into one (axis: [:group, k]) is
683
+ # not supported, as for the order statistics.
684
+ CArray.send(:define_named_methods, self, [:first, :last, :nunique], :group_position)
685
+
686
+ private def group_position (op, **kw)
687
+ has_group, fused = AxisGroup.parse_axis(kw[:axis])
688
+ return value.public_send(op, **kw) unless has_group
689
+ unless fused.empty?
690
+ raise NotImplementedError,
691
+ "axis_group: folding a band into #{op} (axis: [:group, k]) is not " \
692
+ "supported; #{op} gathers every member of a group"
693
+ end
694
+ unknown = kw.keys - [:axis]
695
+ unless unknown.empty?
696
+ raise ArgumentError,
697
+ "#{op}: unknown keyword#{unknown.size > 1 ? "s" : ""}: " +
698
+ unknown.map(&:inspect).join(", ")
699
+ end
700
+ composite_order(op, [])
701
+ end
702
+
537
703
  # @overload sort_addr
538
704
  # Per-group sorted flat source addresses (group-major). Any grouping (single
539
705
  # or composite group slots, rank-1 or rank-N, flat or band-preserving) is
@@ -569,12 +735,13 @@ class CAGroupIterator
569
735
  raise LocalJumpError, "no block given (yield)" unless block
570
736
  ccat, _kd, gslots, bslots, gaxes = composite_layout
571
737
  return value.group_by_category(ccat).map(data_type: data_type, &block) if bslots.empty?
572
- dt = data_type || value.data_type
573
- out = CArray.new(dt, value.shape)
738
+ out = nil
574
739
  each_band_block(ccat, gslots, bslots, gaxes) do |val_idx, _oi, _co, gi|
575
- out[*val_idx] = gi.map(data_type: dt, &block)
740
+ result = gi.map(data_type: data_type, &block)
741
+ out ||= blank_of(result, value.shape)
742
+ out[*val_idx] = result
576
743
  end
577
- out
744
+ out || (data_type ? CArray.new(data_type, value.shape) : blank_of(value, value.shape))
578
745
  end
579
746
 
580
747
  # @overload reduce { |members| ... }
@@ -599,8 +766,50 @@ class CAGroupIterator
599
766
  out
600
767
  end
601
768
 
769
+ protected
770
+
771
+ # min_count: masks a group with fewer contributing cells (present cells,
772
+ # or every cell for a count of the mask); fill_value: then fills the masked
773
+ # cells -- as a core reduction does with each. Protected: wmean finishes
774
+ # through the iterator over its products.
775
+ def finish_reduction (op, kw, result)
776
+ need = CArray.__present_cells_needed_per_result_cell__(op, kw[:min_count], 0)
777
+ if need > 0
778
+ counts = if AxisGroup::MASK_COUNTS.include?(op.to_sym)
779
+ elements(axis: kw[:axis])
780
+ else
781
+ count_not_masked(axis: kw[:axis])
782
+ end
783
+ if result.is_a?(CArray)
784
+ result[counts.lt(need)] = UNDEF
785
+ elsif counts < need
786
+ result = UNDEF
787
+ end
788
+ end
789
+ CArray.__fill_undefined_result_cells__(result, kw[:fill_value])
790
+ end
791
+
602
792
  private
603
793
 
794
+ # An unknown keyword is refused before the work is done.
795
+ def check_reduce_keywords (op, kw)
796
+ allowed = [:all, :any].include?(op.to_sym) ? AxisGroup::LOGICAL_REDUCE_KEYWORDS : AxisGroup::REDUCE_KEYWORDS
797
+ unknown = kw.keys - allowed
798
+ unless unknown.empty?
799
+ raise ArgumentError,
800
+ "#{op}: unknown keyword#{unknown.size > 1 ? "s" : ""}: " +
801
+ unknown.map(&:inspect).join(", ")
802
+ end
803
+ CArray.__present_cells_needed_per_result_cell__(op, kw[:min_count], 0)
804
+ nil
805
+ end
806
+
807
+ # The same grouping over another value.
808
+ def regroup (value, spec)
809
+ self.class.send(:build_for, value, spec)
810
+ end
811
+
812
+
604
813
  # Build the composite categorical over the group block (the sub-array spanning
605
814
  # every group axis). Each group slot -- rank-1 or rank-N -- contributes its
606
815
  # per-axis codes; the slot codes are broadcast into the group-block rank (size
@@ -622,7 +831,8 @@ class CAGroupIterator
622
831
  # askable. Asking a one-cell array of the same data type lets the core's own
623
832
  # refusal through, unworded by us.
624
833
  def boolean_payload! (op)
625
- CArray.new(value.data_type, [1]).public_send(op)
834
+ probe = CAIterator.core_probe(value)
835
+ probe.public_send(op) if probe
626
836
  nil
627
837
  end
628
838
 
@@ -649,6 +859,13 @@ class CAGroupIterator
649
859
  [ccat, kdims, gslots, bslots, gaxes]
650
860
  end
651
861
 
862
+ # With no band axis the group block is the whole value, grouped in one
863
+ # pass. With the fast paths off it goes through the band loop instead,
864
+ # which then has one position.
865
+ def whole_block? (bslots)
866
+ bslots.empty? && CArray.__fast_paths_enabled__
867
+ end
868
+
652
869
  # Iterate the band positions of a band-preserving grouping. For each band
653
870
  # position it yields the group block grouped by the composite categorical,
654
871
  # together with two indices: val_idx (source order -- nil across the group
@@ -696,8 +913,74 @@ class CAGroupIterator
696
913
  "axis_group scan: folding a band into a scan (axis: [:group, k]) " \
697
914
  "is not supported; a scan preserves the source shape"
698
915
  end
916
+ return scan_by_core(op) unless kernel_scans?(op)
699
917
  group_axes, bundles, = spec.reduce_plan([])
700
- value.__axis_group_scan__(group_axes, bundles, op)
918
+ value.__scan_by_axis_groups__(group_axes, bundles, op)
919
+ end
920
+
921
+ # Whether the scatter kernels read this payload: an integer or a float,
922
+ # not a Face. They answer there in the core's data types. With the fast
923
+ # paths off (a development build run with CARRAY_GENERAL_PATH set) each
924
+ # group is reduced by the core instead -- except a band folded into the
925
+ # statistic (+fused+), which has no route but the kernel.
926
+ def kernel_reads_value? (fused = [])
927
+ return false if value.face? || ! (value.integer? || value.float?)
928
+ CArray.__fast_paths_enabled__ || ! fused.empty?
929
+ end
930
+
931
+ # Whether the scan kernel gives the core's answer: it reads an integer,
932
+ # float, boolean or object payload (not a complex, not a Face), and answers cumsum / cumprod in
933
+ # float64 (an object in object), cummax / cummin in the payload's data type
934
+ # and cumcount in int64 -- not the core's answer for a boolean's running sum.
935
+ def kernel_scans? (op)
936
+ return false unless CArray.__fast_paths_enabled__
937
+ return false if value.face? || value.complex?
938
+ return true if value.object? || op == :cumcount
939
+ core = CAIterator.core_probe(value).public_send(op).data_type
940
+ kernel = [:cumsum, :cumprod].include?(op) ? CA_FLOAT64 : value.data_type
941
+ core == kernel
942
+ end
943
+
944
+ # A scan the kernel does not answer as the core does: each group block is
945
+ # grouped through the composite categorical, whose scans are the core's.
946
+ def scan_by_core (op, *args, **kw)
947
+ ccat, _kd, gslots, bslots, gaxes = composite_layout
948
+ return value.group_by_category(ccat).public_send(op, *args, **kw) if whole_block?(bslots)
949
+ out = nil
950
+ each_band_block(ccat, gslots, bslots, gaxes) do |val_idx, _oi, _co, gi|
951
+ result = gi.public_send(op, *args, **kw)
952
+ out ||= blank_of(result, value.shape)
953
+ out[*val_idx] = result
954
+ end
955
+ out || blank_of(value, value.shape)
956
+ end
957
+
958
+ # A reduction of a payload the scatter kernel does not read, group by
959
+ # group through the composite categorical. Folding a band into the
960
+ # statistic needs the kernel.
961
+ def reduce_by_core (op, kw)
962
+ _, fused = AxisGroup.parse_axis(kw[:axis])
963
+ unless fused.empty?
964
+ raise ArgumentError,
965
+ "axis_group #{op}: folding a band into the statistic " \
966
+ "(axis: [:group, k]) is not supported for a #{value.class} of " \
967
+ "data type #{value.data_type_name}"
968
+ end
969
+ check_reduce_keywords(op, kw)
970
+ finish_reduction(op, kw, composite_order(op, []))
971
+ end
972
+
973
+ # An array of +shape+ of the same kind as +model+ -- its data type, or its
974
+ # Face -- with every cell UNDEF. A read-only Face cannot be written cell by
975
+ # cell, so its values are held in an object array.
976
+ def blank_of (model, shape)
977
+ out = if model.face? && model.read_only?
978
+ CArray.new(CA_OBJECT, shape)
979
+ else
980
+ model.__send__(:blank_like, shape)
981
+ end
982
+ out[] = UNDEF
983
+ out
701
984
  end
702
985
 
703
986
  # Dispatch an order statistic (median / percentile / quantile). No :group ->
@@ -707,13 +990,19 @@ class CAGroupIterator
707
990
  # operation, and remains a follow-up.
708
991
  def order_stat (op, args, kw)
709
992
  has_group, fused = AxisGroup.parse_axis(kw[:axis])
710
- return value.public_send(op, *args) unless has_group
993
+ return value.public_send(op, *args, **kw) unless has_group
711
994
  unless fused.empty?
712
995
  raise NotImplementedError,
713
996
  "axis_group: folding a band into an order statistic (axis: [:group, k]) " \
714
997
  "is not supported; order statistics gather every member of a group"
715
998
  end
716
- composite_order(op, args)
999
+ check_reduce_keywords(op, kw)
1000
+ res = composite_order(op, args)
1001
+ if op == :quantile
1002
+ res.map { |r| finish_reduction(op, kw, r) }
1003
+ else
1004
+ finish_reduction(op, kw, res)
1005
+ end
717
1006
  end
718
1007
 
719
1008
  # Per-group order statistic over the composite grouping. A flat grouping (no
@@ -727,20 +1016,20 @@ class CAGroupIterator
727
1016
  def composite_order (op, args)
728
1017
  ccat, kdims, gslots, bslots, gaxes = composite_layout
729
1018
  nout = (op == :quantile) ? 5 : 1
730
- if bslots.empty?
1019
+ if whole_block?(bslots)
731
1020
  res = value.group_by_category(ccat).public_send(op, *args)
732
1021
  return nout == 1 ? res.reshape(*kdims) : res.map { |r| r.reshape(*kdims) }
733
1022
  end
734
1023
  out_shape = spec.slot_meta.map { |m| m[:kind] == :group ? m[:k] : m[:len] }
735
- outs = Array.new(nout) { CArray.float64(*out_shape) }
1024
+ outs = nil
736
1025
  each_band_block(ccat, gslots, bslots, gaxes) do |_vi, out_idx, _co, gi|
737
- result = gi.public_send(op, *args)
738
- if nout == 1
739
- outs[0][*out_idx] = result.reshape(*kdims)
740
- else
741
- result.each_with_index { |r, q| outs[q][*out_idx] = r.reshape(*kdims) }
742
- end
1026
+ results = gi.public_send(op, *args)
1027
+ results = [results] unless results.is_a?(Array) # quantile answers five
1028
+ # each output takes the kind the categorical answered in
1029
+ outs ||= results.map { |r| blank_of(r, out_shape) }
1030
+ results.each_with_index { |r, q| outs[q][*out_idx] = r.reshape(*kdims) }
743
1031
  end
1032
+ outs ||= Array.new(nout) { blank_of(value, out_shape) }
744
1033
  nout == 1 ? outs[0] : outs
745
1034
  end
746
1035
 
@@ -782,7 +1071,15 @@ class CAGroupIterator
782
1071
  end
783
1072
  out
784
1073
  end
1074
+
1075
+ # lag and strip_mask(method:) give the source's values moved or held in
1076
+ # place, so the result shows the source's attributes, as CArray#lag and
1077
+ # CArray#strip_mask(method:) do.
1078
+ prepend(Module.new do
1079
+ def lag (*args, **keywords) = value.__send__(:carry_attrs, super)
1080
+ def strip_mask (**keywords) = value.__send__(:carry_attrs, super)
1081
+ end)
785
1082
  end
786
1083
 
787
1084
  require "carray/categorical"
788
- CArray.__register_axis_group_classes__(CACategorical, AxisGroup)
1085
+ CArray.send(:register_axis_group_classes, CACategorical, AxisGroup)