carray 3.0.3 → 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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +1 -6
  7. data/ext/ca_axis_group.c +27 -27
  8. data/ext/ca_binop_dispatch.c +1 -29
  9. data/ext/ca_binop_dispatch.h +0 -1
  10. data/ext/ca_categorical_iterator.c +205 -134
  11. data/ext/ca_compare.c +7 -18
  12. data/ext/ca_group_iter.c +16 -17
  13. data/ext/ca_kernel_iterator.c +1234 -1994
  14. data/ext/ca_kernel_iterator.h +391 -437
  15. data/ext/ca_monop_dispatch.c +82 -59
  16. data/ext/ca_monop_dispatch.h +1 -0
  17. data/ext/ca_obj_array.c +96 -39
  18. data/ext/ca_obj_bincmp.c +24 -21
  19. data/ext/ca_obj_binop.c +15 -26
  20. data/ext/ca_obj_bitarray.c +3 -2
  21. data/ext/ca_obj_bitfield.c +5 -6
  22. data/ext/ca_obj_block.c +18 -9
  23. data/ext/ca_obj_byte_swap.c +5 -6
  24. data/ext/ca_obj_const_string.c +173 -19
  25. data/ext/ca_obj_face.c +238 -57
  26. data/ext/ca_obj_face.h +19 -0
  27. data/ext/ca_obj_fake.c +15 -29
  28. data/ext/ca_obj_farray.c +6 -3
  29. data/ext/ca_obj_field.c +17 -5
  30. data/ext/ca_obj_fixlen_string.c +2 -3
  31. data/ext/ca_obj_grid.c +406 -132
  32. data/ext/ca_obj_meld.c +172 -34
  33. data/ext/ca_obj_moncmp.c +23 -8
  34. data/ext/ca_obj_monop.c +67 -57
  35. data/ext/ca_obj_object.c +157 -51
  36. data/ext/ca_obj_record.c +12 -7
  37. data/ext/ca_obj_reduce.c +13 -8
  38. data/ext/ca_obj_refer.c +25 -27
  39. data/ext/ca_obj_remap.c +30 -9
  40. data/ext/ca_obj_repeat.c +9 -10
  41. data/ext/ca_obj_roll.c +6 -5
  42. data/ext/ca_obj_select.c +14 -9
  43. data/ext/ca_obj_select_axis.c +15 -47
  44. data/ext/ca_obj_shift.c +11 -1
  45. data/ext/ca_obj_stack.c +179 -34
  46. data/ext/ca_obj_stride.c +162 -115
  47. data/ext/ca_obj_string.c +71 -4
  48. data/ext/ca_obj_tile.c +6 -5
  49. data/ext/ca_obj_time.c +22 -6
  50. data/ext/ca_obj_timedelta.c +22 -6
  51. data/ext/ca_obj_transpose.c +5 -3
  52. data/ext/ca_obj_triop.c +23 -32
  53. data/ext/ca_obj_window.c +190 -26
  54. data/ext/ca_sweep_engine.c +2 -2
  55. data/ext/caframe_csv_reader.c +80 -1
  56. data/ext/carray.h +181 -75
  57. data/ext/carray_access.c +184 -96
  58. data/ext/carray_address_basis.c +343 -87
  59. data/ext/carray_attribute.c +89 -6
  60. data/ext/carray_bincount.c +10 -8
  61. data/ext/carray_broadcast.c +13 -8
  62. data/ext/carray_build_flags.h +3 -0
  63. data/ext/carray_call_cfunc.c +108 -0
  64. data/ext/carray_cast.c +142 -24
  65. data/ext/carray_cast_func.rb +46 -6
  66. data/ext/carray_class.c +4 -3
  67. data/ext/carray_config.h +28 -0
  68. data/ext/carray_conversion.c +16 -7
  69. data/ext/carray_copy.c +51 -14
  70. data/ext/carray_core.c +147 -55
  71. data/ext/carray_count.c +50 -20
  72. data/ext/carray_element.c +76 -33
  73. data/ext/carray_factorize.c +118 -64
  74. data/ext/carray_generate.c +191 -50
  75. data/ext/carray_histogram.c +62 -17
  76. data/ext/carray_hold.c +12 -15
  77. data/ext/carray_index_classifier.c +388 -176
  78. data/ext/carray_internal.h +336 -29
  79. data/ext/carray_kernels_bincmp.c +4453 -0
  80. data/ext/carray_kernels_binop.c +11368 -0
  81. data/ext/carray_kernels_init.c +1734 -0
  82. data/ext/carray_kernels_moncmp.c +2104 -0
  83. data/ext/carray_kernels_monop.c +18369 -0
  84. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  85. data/ext/carray_kernels_reduce_boolean.c +406 -0
  86. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  87. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  88. data/ext/carray_kernels_reduce_variance.c +3849 -0
  89. data/ext/carray_kernels_scan.c +4302 -0
  90. data/ext/carray_kernels_search.c +33829 -0
  91. data/ext/carray_kernels_sort.c +10906 -0
  92. data/ext/carray_kernels_triop.c +1864 -0
  93. data/ext/carray_key_windows.c +216 -0
  94. data/ext/carray_lazy.c +170 -111
  95. data/ext/carray_mask.c +292 -72
  96. data/ext/carray_median_percentile.c +82 -28
  97. data/ext/carray_memory_view.c +669 -561
  98. data/ext/carray_operator.c +124 -47
  99. data/ext/carray_order.c +384 -76
  100. data/ext/carray_partition.c +28 -27
  101. data/ext/carray_random.c +67 -13
  102. data/ext/carray_round.c +273 -0
  103. data/ext/carray_scatter.c +63 -38
  104. data/ext/carray_segment.c +3 -3
  105. data/ext/carray_slab.c +91 -28
  106. data/ext/carray_sort.c +35 -35
  107. data/ext/carray_struct.c +19 -18
  108. data/ext/carray_test.c +26 -5
  109. data/ext/carray_undef.c +4 -0
  110. data/ext/carray_utils.c +246 -45
  111. data/ext/mk_call_cfunc.rb +19 -2
  112. data/ext/mkkernel.rb +198 -240
  113. data/ext/ruby_carray.c +10 -4
  114. data/ext/version.h +4 -4
  115. data/lib/carray/attribute.rb +126 -22
  116. data/lib/carray/autoload_carray.rb +8 -31
  117. data/lib/carray/autoload_method_extension.rb +14 -5
  118. data/lib/carray/axis_group.rb +255 -106
  119. data/lib/carray/basics.rb +78 -10
  120. data/lib/carray/bincount_nd.rb +12 -2
  121. data/lib/carray/block_iterator.rb +126 -23
  122. data/lib/carray/boolean_reduce.rb +17 -17
  123. data/lib/carray/categorical.rb +165 -43
  124. data/lib/carray/categorical_iterator.rb +130 -27
  125. data/lib/carray/complex.rb +5 -5
  126. data/lib/carray/conditional.rb +67 -18
  127. data/lib/carray/const_string.rb +14 -5
  128. data/lib/carray/construct.rb +15 -11
  129. data/lib/carray/data_type_extension.rb +1 -0
  130. data/lib/carray/distance_limit.rb +77 -0
  131. data/lib/carray/fixlen_string.rb +7 -1
  132. data/lib/carray/frame/column_verbs.rb +70 -0
  133. data/lib/carray/frame/convert.rb +69 -0
  134. data/lib/carray/frame/csv_parser.rb +111 -17
  135. data/lib/carray/frame/describe.rb +3 -2
  136. data/lib/carray/frame/frame.rb +246 -21
  137. data/lib/carray/frame/group.rb +510 -95
  138. data/lib/carray/frame/io.rb +115 -28
  139. data/lib/carray/frame/join.rb +312 -51
  140. data/lib/carray/frame/records.rb +83 -7
  141. data/lib/carray/frame/reshape.rb +42 -35
  142. data/lib/carray/frame/sort.rb +7 -19
  143. data/lib/carray/frame/verbs.rb +188 -48
  144. data/lib/carray/frame/windows.rb +117 -0
  145. data/lib/carray/frame.rb +2 -0
  146. data/lib/carray/fuse_source.rb +11 -4
  147. data/lib/carray/fusion.rb +40 -16
  148. data/lib/carray/histogram.rb +6 -1
  149. data/lib/carray/inspect.rb +31 -8
  150. data/lib/carray/iterator.rb +85 -7
  151. data/lib/carray/key_windows.rb +620 -0
  152. data/lib/carray/lazy.rb +182 -183
  153. data/lib/carray/mask_gap_fill.rb +63 -57
  154. data/lib/carray/meld_reduce.rb +12 -14
  155. data/lib/carray/methods/bin.rb +26 -18
  156. data/lib/carray/methods/bincount.rb +16 -0
  157. data/lib/carray/methods/bit_string.rb +1 -1
  158. data/lib/carray/methods/choose.rb +16 -3
  159. data/lib/carray/methods/composition.rb +3 -1
  160. data/lib/carray/methods/gather_nd.rb +14 -5
  161. data/lib/carray/methods/insert_block.rb +2 -2
  162. data/lib/carray/methods/is_between.rb +59 -0
  163. data/lib/carray/methods/is_in.rb +53 -36
  164. data/lib/carray/methods/join.rb +1 -6
  165. data/lib/carray/methods/lag.rb +49 -0
  166. data/lib/carray/methods/locate_addr.rb +47 -51
  167. data/lib/carray/methods/mode.rb +1 -1
  168. data/lib/carray/methods/nunique.rb +11 -6
  169. data/lib/carray/methods/resize.rb +3 -3
  170. data/lib/carray/methods/value_counts.rb +4 -11
  171. data/lib/carray/record_order.rb +3 -3
  172. data/lib/carray/rng.rb +3 -0
  173. data/lib/carray/runtime.rb +53 -8
  174. data/lib/carray/segment_iterator.rb +313 -82
  175. data/lib/carray/serialize.rb +24 -10
  176. data/lib/carray/slab_iterator.rb +50 -0
  177. data/lib/carray/string.rb +105 -5
  178. data/lib/carray/string_operation_extension.rb +8 -1
  179. data/lib/carray/struct.rb +9 -21
  180. data/lib/carray/struct_builder.rb +19 -8
  181. data/lib/carray/time.rb +726 -104
  182. data/lib/carray/window_iterator.rb +122 -16
  183. data/lib/carray.rb +7 -0
  184. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  185. data/yard-stubs/ca_obj_field.rb +4 -0
  186. data/yard-stubs/ca_obj_grid.rb +21 -10
  187. data/yard-stubs/ca_obj_refer.rb +2 -1
  188. data/yard-stubs/ca_obj_roll.rb +2 -2
  189. data/yard-stubs/ca_obj_shift.rb +5 -1
  190. data/yard-stubs/ca_obj_tile.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +8 -3
  192. data/yard-stubs/carray_access.rb +6 -1
  193. data/yard-stubs/carray_attribute.rb +0 -5
  194. data/yard-stubs/carray_cast.rb +10 -0
  195. data/yard-stubs/carray_conversion.rb +2 -1
  196. data/yard-stubs/carray_copy.rb +4 -2
  197. data/yard-stubs/carray_count.rb +4 -4
  198. data/yard-stubs/carray_element.rb +10 -4
  199. data/yard-stubs/carray_generate.rb +21 -1
  200. data/yard-stubs/carray_mask.rb +26 -2
  201. data/yard-stubs/carray_math.rb +27 -1
  202. data/yard-stubs/carray_median_percentile.rb +5 -2
  203. data/yard-stubs/carray_memory_view.rb +40 -23
  204. data/yard-stubs/carray_order.rb +29 -18
  205. data/yard-stubs/carray_random.rb +13 -4
  206. data/yard-stubs/carray_scatter.rb +14 -9
  207. data/yard-stubs/carray_slab.rb +9 -2
  208. data/yard-stubs/carray_sort.rb +6 -5
  209. data/yard-stubs/carray_test.rb +5 -3
  210. metadata +25 -3
  211. data/lib/carray/arrow_tensor.rb +0 -401
  212. data/lib/carray/table.rb +0 -108
@@ -41,7 +41,7 @@ class CASegmentIterator < CAIterator
41
41
  kw = {}
42
42
  kw[:offsets] = offsets unless offsets.nil?
43
43
  kw[:lengths] = lengths unless lengths.nil?
44
- bounds = CArray.__segment_bounds__("segments", **kw)
44
+ bounds = CArray.__segment_boundaries_from_lengths_or_offsets__("segments", **kw)
45
45
  origin, finish = bounds[0], bounds[-1]
46
46
  if origin < 0 || finish > value.elements
47
47
  raise ArgumentError,
@@ -58,8 +58,8 @@ class CASegmentIterator < CAIterator
58
58
  @offsets = @bounds[0...-1].copy # the starts, for the kernels
59
59
  @elements = @k > 0 ? (@bounds[1..-1] - @bounds[0...-1]) : CArray.int64(0)
60
60
  @grouped = finish > origin ? value.reshape(value.elements)[origin...finish].copy
61
- : CArray.new(value.data_type, [0], bytes: value.bytes)
62
- @empty = CArray.new(@grouped.data_type, [0], bytes: @grouped.bytes)
61
+ : value.reshape(value.elements)[0...0].copy
62
+ @empty = @grouped[0...0] # an empty segment is the values' own kind
63
63
  end
64
64
 
65
65
  # @overload inspect
@@ -153,7 +153,7 @@ class CASegmentIterator < CAIterator
153
153
  def sum(axis: nil)
154
154
  return axis_sum(axis) if axis
155
155
  m = moments
156
- return per_segment(core_reduce_type(:sum)) { |s| s.sum } unless m
156
+ return per_segment(core_reduce_kind(:sum)) { |s| s.sum } unless m
157
157
  m[:sum].copy # the moments sum IS the core fold (empty -> 0.0)
158
158
  end
159
159
 
@@ -167,7 +167,7 @@ class CASegmentIterator < CAIterator
167
167
  # @return [CArray]
168
168
  def accumulate(axis: nil)
169
169
  return axis_by_masked_copy(axis, :accumulate, core_reduce_type(:accumulate)) if axis
170
- per_segment(core_reduce_type(:accumulate)) { |s| s.accumulate }
170
+ per_segment(core_reduce_kind(:accumulate)) { |s| s.accumulate }
171
171
  end
172
172
 
173
173
  # @overload max
@@ -177,7 +177,7 @@ class CASegmentIterator < CAIterator
177
177
  def max(axis: nil)
178
178
  return axis_moments(axis, :max)[:max] if axis
179
179
  m = moments
180
- m ? m[:max].copy : per_segment(core_reduce_type(:max)) { |s| s.max }
180
+ m ? m[:max].copy : per_segment(core_reduce_kind(:max)) { |s| s.max }
181
181
  end
182
182
 
183
183
  # @overload min
@@ -187,7 +187,7 @@ class CASegmentIterator < CAIterator
187
187
  def min(axis: nil)
188
188
  return axis_moments(axis, :min)[:min] if axis
189
189
  m = moments
190
- m ? m[:min].copy : per_segment(core_reduce_type(:min)) { |s| s.min }
190
+ m ? m[:min].copy : per_segment(core_reduce_kind(:min)) { |s| s.min }
191
191
  end
192
192
 
193
193
  # @overload mean
@@ -196,7 +196,7 @@ class CASegmentIterator < CAIterator
196
196
  def mean(axis: nil)
197
197
  return axis_mean(axis) if axis
198
198
  m = moments
199
- return per_segment(core_reduce_type(:mean)) { |s| s.mean } unless m
199
+ return per_segment(core_reduce_kind(:mean)) { |s| s.mean } unless m
200
200
  cnt = m[:count]
201
201
  out = m[:sum] / cnt.float64 # count 0 -> NaN, masked next
202
202
  out[cnt.eq(0)] = UNDEF # empty / all-masked category -> MASKED
@@ -220,8 +220,8 @@ class CASegmentIterator < CAIterator
220
220
  # @return [CArray]
221
221
  def percentile (p, axis: nil)
222
222
  return axis_order_stat(axis, :percentile, p) if axis
223
- unless MONOID_TYPES.include?(grouped.data_type)
224
- return per_segment(core_reduce_type(:percentile, p)) { |s| s.percentile(p) }
223
+ unless kernel_reads?
224
+ return per_segment(core_reduce_kind(:percentile, p)) { |s| s.percentile(p) }
225
225
  end
226
226
  out = CArray.float64(@k)
227
227
  grouped.send(:__reduceat_percentile__, @offsets, p.to_f, out)
@@ -235,8 +235,8 @@ class CASegmentIterator < CAIterator
235
235
  # MASKED. For a single fraction q in 0..1 use `percentile(q * 100)`.
236
236
  # @return [Array<CArray>]
237
237
  def quantile
238
- unless MONOID_TYPES.include?(grouped.data_type)
239
- return CArray.__answer_as__(:quantile) { [0, 25, 50, 75, 100].map { |p| percentile(p) } }
238
+ unless kernel_reads?
239
+ return CArray.__name_errors_after_method__(:quantile) { [0, 25, 50, 75, 100].map { |p| percentile(p) } }
240
240
  end
241
241
  outs = Array.new(5) { CArray.float64(@k) }
242
242
  grouped.send(:__reduceat_quantile__, @offsets, *outs)
@@ -251,7 +251,7 @@ class CASegmentIterator < CAIterator
251
251
  def variance(axis: nil)
252
252
  return axis_by_masked_copy(axis, :variance) if axis
253
253
  m = moments
254
- return per_segment(core_reduce_type(:variance)) { |s| s.variance } unless m
254
+ return per_segment(core_reduce_kind(:variance)) { |s| s.variance } unless m
255
255
  cnt = m[:count]
256
256
  means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1,
257
257
  out = CArray.float64(@k) # ignored by the kernel's n<2 guards)
@@ -267,7 +267,7 @@ class CASegmentIterator < CAIterator
267
267
  def stddev(axis: nil)
268
268
  return axis_by_masked_copy(axis, :stddev) if axis
269
269
  m = moments
270
- return per_segment(core_reduce_type(:stddev)) { |s| s.stddev } unless m
270
+ return per_segment(core_reduce_kind(:stddev)) { |s| s.stddev } unless m
271
271
  variance.sqrt # sqrt propagates the n<2 mask
272
272
  end
273
273
 
@@ -278,7 +278,7 @@ class CASegmentIterator < CAIterator
278
278
  # @return [CArray]
279
279
  def prod(axis: nil)
280
280
  return axis_prod(axis) if axis
281
- return per_segment(core_reduce_type(:prod)) { |s| s.prod } unless MONOID_TYPES.include?(grouped.data_type)
281
+ return per_segment(core_reduce_kind(:prod)) { |s| s.prod } unless kernel_reads?
282
282
  out = CArray.float64(@k)
283
283
  grouped.send(:__reduceat_prod__, @offsets, out)
284
284
  out
@@ -304,6 +304,48 @@ class CASegmentIterator < CAIterator
304
304
  aa ? aa[:any] : per_segment(CA_BOOLEAN) { |s| s.any }
305
305
  end
306
306
 
307
+ # @overload first
308
+ # Returns the first present value of each segment, in the value's data
309
+ # type (matching `CArray#first`). A segment with no present value is
310
+ # UNDEF.
311
+ # @return [CArray]
312
+ def first (axis: nil)
313
+ return axis_by_core(axis, :first) if axis
314
+ edge_values(:first)
315
+ end
316
+
317
+ # @overload last
318
+ # Returns the last present value of each segment, in the value's data
319
+ # type (matching `CArray#last`). A segment with no present value is
320
+ # UNDEF.
321
+ # @return [CArray]
322
+ def last (axis: nil)
323
+ return axis_by_core(axis, :last) if axis
324
+ edge_values(:last)
325
+ end
326
+
327
+ # @overload nunique
328
+ # Returns the number of distinct present values in each segment as int64,
329
+ # with the distinctness of `CArray#nunique` (every NaN is one value,
330
+ # -0.0 is 0.0). A segment with no present value counts 0.
331
+ # @return [CArray]
332
+ def nunique (axis: nil)
333
+ return axis_by_core(axis, :nunique) if axis
334
+ return per_segment(CA_INT64) { |s| s.nunique } unless CArray.__fast_paths_enabled__
335
+ out = CArray.int64(@k)
336
+ return out if grouped.elements.zero?
337
+ # Each cell's value becomes a code, so a distinct (segment, value) pair is
338
+ # a distinct int64 key; the keys' segments, counted, are the answer.
339
+ values = grouped.categorize
340
+ width = [values.labels.size, 1].max
341
+ keys = CArray.segment_index(offsets: @bounds) * width + values.codes.int64
342
+ owners = keys.unique / width
343
+ return out if owners.elements.zero?
344
+ counts = owners.bincount
345
+ out[0...counts.elements] = counts
346
+ out
347
+ end
348
+
307
349
  # ---- tier 2 (fused / population / position) ------------------------------
308
350
 
309
351
  # @overload minmax
@@ -324,17 +366,19 @@ class CASegmentIterator < CAIterator
324
366
 
325
367
  # @overload variancep
326
368
  # Per-segment POPULATION variance (ddof=0) as float64, matching
327
- # `CArray#variancep`: empty / all-masked -> MASKED, single value -> 0.0.
369
+ # `CArray#variancep`: empty / all-masked -> MASKED, single value -> 0.0
370
+ # (NaN when that value is a NaN or an infinity).
328
371
  # Derived from the sample variance (variancep = variance * (n-1) / n), so it
329
372
  # reuses the centred two-pass kernel with no extra walk.
330
373
  # @return [CArray]
331
374
  def variancep(axis: nil)
332
375
  return axis_by_masked_copy(axis, :variancep) if axis
333
376
  m = moments
334
- return per_segment(core_reduce_type(:variancep)) { |s| s.variancep } unless m
377
+ return per_segment(core_reduce_kind(:variancep)) { |s| s.variancep } unless m
335
378
  cnt = m[:count]
336
379
  vp = variance * (cnt - 1).float64 / cnt.float64
337
- vp[cnt.eq(1)] = 0.0 # one value: defined, unlike variance
380
+ one = cnt.eq(1) # one value: defined, unlike variance --
381
+ vp[one] = (m[:sum] - m[:sum])[one] # 0.0, or NaN for a NaN or an infinity
338
382
  vp
339
383
  end
340
384
 
@@ -344,7 +388,7 @@ class CASegmentIterator < CAIterator
344
388
  def stddevp(axis: nil)
345
389
  return axis_by_masked_copy(axis, :stddevp) if axis
346
390
  m = moments
347
- return per_segment(core_reduce_type(:stddevp)) { |s| s.stddevp } unless m
391
+ return per_segment(core_reduce_kind(:stddevp)) { |s| s.stddevp } unless m
348
392
  variancep.sqrt
349
393
  end
350
394
 
@@ -421,7 +465,7 @@ class CASegmentIterator < CAIterator
421
465
  # @return [CArray]
422
466
  def wsum (weights, axis: nil)
423
467
  return axis_wsum_wmean(weights, axis, :wsum)[0] if axis
424
- kernel = MONOID_TYPES.include?(grouped.data_type)
468
+ kernel = kernel_reads?
425
469
  wg = scatter_weights(weights, kernel)
426
470
  return kernel_weighted(wg)[0] if kernel
427
471
  fold_weighted(:wsum, wg, 0.0) { |v, ws| v.wsum(ws) }
@@ -435,7 +479,7 @@ class CASegmentIterator < CAIterator
435
479
  # @return [CArray]
436
480
  def wmean (weights, axis: nil)
437
481
  return axis_wsum_wmean(weights, axis, :wmean)[1] if axis
438
- kernel = MONOID_TYPES.include?(grouped.data_type)
482
+ kernel = kernel_reads?
439
483
  wg = scatter_weights(weights, kernel)
440
484
  return kernel_weighted(wg)[1] if kernel
441
485
  fold_weighted(:wmean, wg, UNDEF) { |v, ws| v.wmean(ws) }
@@ -507,7 +551,7 @@ class CASegmentIterator < CAIterator
507
551
  # data type is the core's (`cumsum` of a boolean is uint64, of an integer
508
552
  # float64), a Face keeps its Face (`cummin` of a time column is a time), and
509
553
  # a scan the core refuses (`cumsum` of a time column) raises in the core's
510
- # words. When the fused C kernel __axis_group_scan__ answers in that data
554
+ # words. When the fused C kernel __scan_by_axis_groups__ answers in that data
511
555
  # type, it runs with the whole source as a single grouped axis and the
512
556
  # segment of each cell (#codes) as the single bundle, emitting in source
513
557
  # order; otherwise each segment is scanned by the core and scattered back.
@@ -527,8 +571,122 @@ class CASegmentIterator < CAIterator
527
571
  # @!method cumcount
528
572
  # Per-segment 1-based within-segment ordinal (int64), source-shaped.
529
573
  # @return [CArray]
530
- [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op|
531
- define_method(op) { |axis: nil| axis ? axis_scan(op, axis) : scan(op) }
574
+ CArray.send(:define_named_methods, self,
575
+ [:cumsum, :cumprod, :cummax, :cummin, :cumcount], :segment_scan)
576
+
577
+ private def segment_scan (op, axis: nil)
578
+ axis ? axis_scan(op, axis) : scan(op)
579
+ end
580
+
581
+ # @overload lag(n = 1)
582
+ # Returns the value `n` steps earlier within each segment, source-shaped,
583
+ # as `CArray#lag` does along an axis. A cell whose segment has no value
584
+ # `n` steps back is UNDEF, so a lag never reaches into the segment before;
585
+ # a cell in no segment is UNDEF. A negative `n` looks ahead.
586
+ # @param n [Integer]
587
+ # @return [CArray]
588
+ def lag (n = 1, axis: nil)
589
+ unless n.is_a?(Integer)
590
+ raise TypeError, "lag: n must be an Integer (got #{n.class})"
591
+ end
592
+ return axis_lag(n, axis) if axis
593
+ out = blank_like(@value, @src_shape)
594
+ out[] = UNDEF
595
+ m = grouped.elements
596
+ return out if m.zero?
597
+ # The copy is in segment order, so one shift moves every segment; the
598
+ # cells it brought across a segment boundary are then cut.
599
+ lagged = grouped.lag(n)
600
+ lagged = out.data_type == CA_OBJECT && lagged.face? ?
601
+ CArray.object(m) { lagged.to_a } : lagged.copy
602
+ unless n.zero?
603
+ seg = CArray.segment_index(offsets: @bounds)
604
+ start = @bounds[seg]
605
+ pos = CArray.int64(m).seq! - start
606
+ lagged[n > 0 ? pos.lt(n) : pos.ge(@bounds[seg + 1] - start + n)] = UNDEF
607
+ end
608
+ out.reshape(@value.elements)[perm] = lagged
609
+ out
610
+ end
611
+
612
+ # @overload strip_mask(method:)
613
+ # Returns the values with each masked cell filled from its segment, as
614
+ # `CArray#strip_mask(method:)` fills a 1-D array: `:forward` (`:ffill`)
615
+ # carries the segment's last present value forward, `:backward`
616
+ # (`:bfill`) its next one back. A fill never reaches across into another
617
+ # segment, so a masked cell with nothing to carry in its segment stays
618
+ # masked; a cell in no segment is UNDEF. The value is not modified.
619
+ # @param method [Symbol] `:forward`, `:ffill`, `:backward` or `:bfill`.
620
+ # @return [CArray] source-shaped
621
+ def strip_mask (method:, axis: nil)
622
+ backward = CAIterator.hold_backward?(method)
623
+ return axis_hold(method, axis) if axis
624
+ if @value.face? && ! face_output?
625
+ return scatter_segments(->(shape) { blank_like(@value, shape) }) { |s| s.strip_mask(method: method) }
626
+ end
627
+ out = blank_like(@value, @src_shape)
628
+ out[] = UNDEF
629
+ m = grouped.elements
630
+ return out if m.zero?
631
+ # For each cell, the present cell its value comes from: the nearest one at
632
+ # or before it (forward) or at or after it (backward), kept when it lies
633
+ # in the cell's own segment.
634
+ slot = CArray.int64(m).seq!
635
+ slot[grouped.is_masked] = backward ? m : -1 if grouped.has_mask?
636
+ seg = CArray.segment_index(offsets: @bounds)
637
+ if backward
638
+ source = slot.reverse.cummin.reverse
639
+ found = source.lt(@bounds[seg + 1])
640
+ else
641
+ source = slot.cummax
642
+ found = source.ge(@bounds[seg])
643
+ end
644
+ filled = grouped[source.pmin(m - 1).pmax(0)].copy
645
+ filled[found.not] = UNDEF
646
+ out.reshape(@value.elements)[perm] = filled
647
+ out
648
+ end
649
+
650
+ # @overload order(descending: false, method: :ordinal, kind: :quick)
651
+ # Returns each cell's rank among the members of its segment, source-shaped
652
+ # int64, as `CArray#order` ranks a 1-D array: `0` is the smallest, ties
653
+ # are broken by position (`:ordinal`) or share a rank (`:dense`), and
654
+ # `descending:` reverses the order. A masked cell, and a cell in no
655
+ # segment, is UNDEF.
656
+ # @return [CArray]
657
+ def order (axis: nil, descending: false, method: :ordinal, kind: :quick)
658
+ return axis_order(axis, descending: descending, method: method, kind: kind) if axis
659
+ out = CArray.int64(*@src_shape)
660
+ out[] = UNDEF
661
+ # The keywords are checked by the core, on no cells, so a segment
662
+ # iterator refuses what CArray#order refuses.
663
+ CArray.int64(0).order(descending: descending, method: method, kind: kind)
664
+ # The core ranks every value once, so the order and the ties are its own.
665
+ # Ranked in the asked direction, so tied cells keep their order either way.
666
+ ranks = grouped.order(method: :dense, kind: kind, descending: descending)
667
+ m = grouped.elements
668
+ return out if m.zero?
669
+ seg = CArray.segment_index(offsets: @bounds)
670
+ present = ranks.is_not_masked
671
+ # One key per cell, segment first and a masked cell last in its segment;
672
+ # a stable sort keeps tied cells in their order.
673
+ key = seg * (m + 1) + ranks.strip_mask(m)
674
+ sorted = key.sort_index(kind: :stable)
675
+ first = @bounds[seg[sorted]] # where each sorted cell's segment starts
676
+ local = CArray.int64(m)
677
+ if method == :dense
678
+ skey = key[sorted]
679
+ step = CArray.int64(m)
680
+ step[] = 1
681
+ step[1..-1] = skey[1..-1].ne(skey[0...-1]).int64 if m > 1
682
+ run = step.cumsum.int64
683
+ local[sorted] = run - run[first]
684
+ else
685
+ local[sorted] = CArray.int64(m).seq! - first
686
+ end
687
+ local[present.not] = UNDEF
688
+ out.reshape(@value.elements)[perm] = local
689
+ out
532
690
  end
533
691
 
534
692
  private
@@ -557,7 +715,7 @@ class CASegmentIterator < CAIterator
557
715
  # one that no cell belongs to: every cell comes back UNDEF, as it does when
558
716
  # there are segments and a cell is in none of them.
559
717
  def scan (op)
560
- CArray.__answer_as__(op) { scan_answered(op) }
718
+ CArray.__name_errors_after_method__(op) { scan_answered(op) }
561
719
  end
562
720
 
563
721
  def scan_answered (op)
@@ -570,14 +728,14 @@ class CASegmentIterator < CAIterator
570
728
  bundle = @k > 0 ? [codes, @k, [0]]
571
729
  : [CArray.int64(@value.elements).fill(-1), 1, [0]]
572
730
  scan_source.reshape(@value.elements)
573
- .__axis_group_scan__([0], [bundle], op)
731
+ .__scan_by_axis_groups__([0], [bundle], op)
574
732
  .reshape(*@src_shape)
575
733
  end
576
734
 
577
735
  # The core's scan of one cell of the value: the scan answers in its data
578
736
  # type and Face. A scan the core refuses raises here, in the core's words.
579
737
  def core_scan_answer (op)
580
- (@core_scan_answer ||= {})[op] ||= core_probe.public_send(op)
738
+ (@core_scan_answer ||= {})[op] ||= CAIterator.core_probe(@value).public_send(op)
581
739
  end
582
740
 
583
741
  # Whether the axis-group scan kernel gives the core's answer. It reads
@@ -587,6 +745,7 @@ class CASegmentIterator < CAIterator
587
745
  # cumcount in int64 -- which is not the core's answer for every data type
588
746
  # (the core counts a boolean's running sum in uint64).
589
747
  def kernel_scans? (op, answer)
748
+ return false unless CArray.__fast_paths_enabled__
590
749
  return false if @value.face? || answer.face? || @value.complex?
591
750
  kernel_type =
592
751
  case op
@@ -634,7 +793,7 @@ class CASegmentIterator < CAIterator
634
793
  if model.face? && model.read_only?
635
794
  CArray.new(CA_OBJECT, shape)
636
795
  else
637
- model.__send__(:__blank_like__, shape)
796
+ model.__send__(:blank_like, shape)
638
797
  end
639
798
  end
640
799
 
@@ -683,8 +842,9 @@ class CASegmentIterator < CAIterator
683
842
 
684
843
  # Lay a per-cell weight array out in segment-contiguous order (same layout as
685
844
  # @grouped), so wsum / wmean can pair each segment's values with its weights.
686
- # Weights are coerced to float64; the same counting-sort scatter propagates
687
- # the weight mask and skips excluded cells, so wg lines up with @grouped.
845
+ # Weights are coerced to float64 and gathered through the same permutation
846
+ # as the values, which carries the weight mask and leaves out the excluded
847
+ # cells, so wg lines up with @grouped.
688
848
  # The weighted kernel reads float64 weights; the per-segment fallback hands
689
849
  # them to the core as they are, so an object weight stays exact.
690
850
  def scatter_weights (weights, as_float64 = true)
@@ -694,10 +854,10 @@ class CASegmentIterator < CAIterator
694
854
  "value.elements (#{codes.elements})"
695
855
  end
696
856
  wf = as_float64 ? weights.float64 : weights
697
- wg = CArray.new(wf.data_type, [grouped.elements], bytes: wf.bytes)
698
- codes.send(:__categorical_scatter__, wf.reshape(wf.elements),
699
- @offsets.copy, wg, @k)
700
- wg
857
+ wf = wf.reshape(wf.elements)
858
+ # An empty index into an empty source is out of range, so take the empty
859
+ # layout directly.
860
+ grouped.elements > 0 ? wf[perm].copy : wf[0...0].copy
701
861
  end
702
862
 
703
863
  # Map a per-segment index to the flat source address via the
@@ -735,6 +895,37 @@ class CASegmentIterator < CAIterator
735
895
  out
736
896
  end
737
897
 
898
+ # The value at the first or last present cell of each segment, UNDEF for a
899
+ # segment with none. Every segment's position is found at once: for each
900
+ # grouped slot, the nearest present slot at or after it (a running minimum
901
+ # taken from the end) or at or before it (a running maximum), read at the
902
+ # segment's first or last slot and kept when it falls inside the segment.
903
+ # A read-only Face cannot take UNDEF cell by cell, so its answers are
904
+ # collected segment by segment, as its other reductions are.
905
+ def edge_values (op)
906
+ n = grouped.elements
907
+ if n.zero? || (@value.face? && ! face_output?) || ! CArray.__fast_paths_enabled__
908
+ return per_segment(core_reduce_kind(op)) { |s| s.public_send(op) }
909
+ end
910
+ slot = CArray.int64(n).seq!
911
+ if grouped.has_mask?
912
+ slot[grouped.is_masked] = (op == :first) ? n : -1
913
+ end
914
+ lo, hi = @bounds[0...-1], @bounds[1..-1]
915
+ if op == :first
916
+ nearest = slot.reverse.cummin.reverse
917
+ at = nearest[lo.pmin(n - 1)]
918
+ else
919
+ nearest = slot.cummax
920
+ at = nearest[(hi - 1).pmax(0)]
921
+ end
922
+ found = at.ge(lo) & at.lt(hi)
923
+ out = grouped[at.pmin(n - 1).pmax(0)].copy
924
+ out[found.not] = UNDEF
925
+ # A reduction carries no attributes, a picked value included.
926
+ grouped.__send__(:drop_carried_attrs, out)
927
+ end
928
+
738
929
  # The members of segment `c` as a CArray slice of the grouped copy. An empty
739
930
  # segment (zero-width segment) yields the shared empty array — a zero-length
740
931
  # slice cannot be taken directly, and an empty array carries the same reduction
@@ -754,10 +945,19 @@ class CASegmentIterator < CAIterator
754
945
  MONOID_TYPES = %i[int8 uint8 int16 uint16 int32 uint32
755
946
  int64 uint64 float32 float64].freeze
756
947
 
948
+ # Whether the C reduceat and scatter kernels read a value of +data_type+:
949
+ # a numeric one, with the fast paths on. They are a faster way to the
950
+ # core's answer for each group; a development build run with
951
+ # CARRAY_GENERAL_PATH set reduces every group by the core instead, which
952
+ # is how the two are compared.
953
+ def kernel_reads? (data_type = grouped.data_type)
954
+ MONOID_TYPES.include?(data_type) && CArray.__fast_paths_enabled__
955
+ end
956
+
757
957
  def moments
758
958
  return @moments if defined?(@moments)
759
959
  @moments =
760
- if MONOID_TYPES.include?(grouped.data_type)
960
+ if kernel_reads?
761
961
  dt = grouped.data_type
762
962
  counts = CArray.int64(@k)
763
963
  sums = CArray.float64(@k)
@@ -773,7 +973,7 @@ class CASegmentIterator < CAIterator
773
973
  def arg_minmax
774
974
  return @arg_minmax if defined?(@arg_minmax)
775
975
  @arg_minmax =
776
- if MONOID_TYPES.include?(grouped.data_type)
976
+ if kernel_reads?
777
977
  mn = CArray.int64(@k)
778
978
  mx = CArray.int64(@k)
779
979
  grouped.send(:__reduceat_argminmax__, @offsets, mn, mx)
@@ -787,7 +987,7 @@ class CASegmentIterator < CAIterator
787
987
  def all_any
788
988
  return @all_any if defined?(@all_any)
789
989
  @all_any =
790
- if grouped.data_type == CA_BOOLEAN
990
+ if grouped.data_type == CA_BOOLEAN && CArray.__fast_paths_enabled__
791
991
  a = CArray.boolean(@k)
792
992
  o = CArray.boolean(@k)
793
993
  grouped.send(:__reduceat_all_any__, @offsets, a, o)
@@ -807,20 +1007,18 @@ class CASegmentIterator < CAIterator
807
1007
  # per-fiber form -- and `accumulate(axis:)`, the one axis: member that needs
808
1008
  # this, is exactly the case that would have found it missing.
809
1009
  def core_reduce_type (op, *args)
810
- (@core_reduce_type ||= {})[[op, args]] ||=
811
- @value.face? && @value.elements.zero? ?
812
- CA_OBJECT : core_probe.public_send(op, *args, axis: 1).data_type
1010
+ kind = core_reduce_kind(op, *args)
1011
+ kind ? kind.data_type : CA_OBJECT
813
1012
  end
814
1013
 
815
- # A one-cell array of the same kind as the values, so the core answers about
816
- # the same thing the segment slices will hand back. For a Face that is a view
817
- # of the values themselves rather than a blank: a Face cannot be allocated
818
- # from its surface data type alone, and a blank storage array is not a valid
819
- # Face for every one of them -- a const string's record indexes a shared
820
- # pool, so a zeroed record points nowhere.
821
- def core_probe
822
- return CArray.new(@value.data_type, [1, 1], bytes: @value.bytes) unless @value.face?
823
- @value.reshape(@value.elements)[[0]].reshape(1, 1)
1014
+ # The core's answer itself for the one-cell probe, or nil for a Face with no
1015
+ # cell to probe. Its class is the one the per-segment answers come in, which
1016
+ # for a Face need not be the values' own (a CATime's spread is a CATimedelta).
1017
+ def core_reduce_kind (op, *args)
1018
+ memo = (@core_reduce_kind ||= {})
1019
+ return memo[[op, args]] if memo.key?([op, args])
1020
+ probe = CAIterator.core_probe(@value)
1021
+ memo[[op, args]] = probe && probe.public_send(op, *args, axis: 1)
824
1022
  end
825
1023
 
826
1024
  # Whether an output can be built by lifting one. A Face is filled by writing
@@ -838,15 +1036,22 @@ class CASegmentIterator < CAIterator
838
1036
  # segment's members — the mask carries the "insufficient present data" contract
839
1037
  # for free (an all-masked segment reduces like an empty one; identity-bearing
840
1038
  # reductions return their identity, ratios return UNDEF).
841
- def per_segment (data_type)
842
- out = if ! @value.face? || data_type != @value.data_type
843
- bytes = (data_type == @value.data_type) ? @value.bytes : 0
1039
+ def per_segment (kind)
1040
+ # a data type the values' own Face answers in names that Face
1041
+ kind = @value if ! kind.is_a?(CArray) && @value.face? && kind == @value.data_type
1042
+ data_type = kind.is_a?(CArray) ? kind.data_type : kind
1043
+ out = if kind.nil?
1044
+ CArray.new(CA_OBJECT, [@k])
1045
+ elsif ! (kind.is_a?(CArray) && kind.face?)
1046
+ bytes = if kind.is_a?(CArray) then kind.bytes
1047
+ elsif data_type == @value.data_type then @value.bytes
1048
+ else 0 end
844
1049
  CArray.new(data_type, [@k], bytes: bytes)
845
- elsif face_output?
846
- # the core answered in the values' own Face, so the output is one
847
- # too: CATime#min hands back a CATime::Element, which only a
848
- # CATime has anywhere to put
849
- @value.__send__(:__blank_like__, [@k])
1050
+ elsif ! kind.read_only?
1051
+ # the core answered in a Face, so the output is one too:
1052
+ # CATime#min hands back a CATime::Element, which only a CATime
1053
+ # has anywhere to put
1054
+ kind.__send__(:blank_like, [@k])
850
1055
  else
851
1056
  # a read-only Face cannot be filled, so its answers are collected
852
1057
  # as the surface objects they already are
@@ -860,6 +1065,10 @@ class CASegmentIterator < CAIterator
860
1065
 
861
1066
  # ---- no axis: form -------------------------------------------------------
862
1067
 
1068
+ def axis_by_core (*)
1069
+ raise NotImplementedError, "#{self.class} has no axis: form"
1070
+ end
1071
+
863
1072
  def axis_by_masked_copy (*)
864
1073
  raise NotImplementedError, "#{self.class} has no axis: form"
865
1074
  end
@@ -884,6 +1093,18 @@ class CASegmentIterator < CAIterator
884
1093
  raise NotImplementedError, "#{self.class} has no axis: form"
885
1094
  end
886
1095
 
1096
+ def axis_lag (*)
1097
+ raise NotImplementedError, "#{self.class} has no axis: form"
1098
+ end
1099
+
1100
+ def axis_order (*)
1101
+ raise NotImplementedError, "#{self.class} has no axis: form"
1102
+ end
1103
+
1104
+ def axis_hold (*)
1105
+ raise NotImplementedError, "#{self.class} has no axis: form"
1106
+ end
1107
+
887
1108
  def axis_map (*)
888
1109
  raise NotImplementedError, "#{self.class} has no axis: form"
889
1110
  end
@@ -931,37 +1152,47 @@ class CASegmentIterator < CAIterator
931
1152
  (op == :count && (args.empty? || args[0].equal?(UNDEF)))
932
1153
  end
933
1154
 
934
- REDUCTIONS.each do |op|
935
- define_method(op) do |*args, min_count: nil, fill_value: nil, **kw|
936
- # The segment reduction already leaves an empty segment as the core
937
- # does, so only a min_count asking for more is applied here.
938
- need = CArray.__min_count_need__(op, min_count, 0)
939
- # an error from the core reductions a member is made of names the member
940
- result = CArray.__answer_as__(op) { super(*args, **kw) }
941
- return result if need == 0 && fill_value.nil?
942
- if need > 0
943
- present = if op == :wsum || op == :wmean
944
- present_pair_count(args[0], kw[:axis])
945
- elsif ReductionCountAndFill.mask_count?(op, args)
946
- if kw[:axis]
947
- raise NotImplementedError,
948
- "#{self.class}##{op}(axis:, min_count:) is not implemented"
949
- end
950
- elements
951
- else
952
- count_not_masked(**kw)
1155
+ CArray.send(:define_named_methods, self, REDUCTIONS, :reduce_counted, super_block: true)
1156
+
1157
+ # The segment reduction already leaves an empty segment as the core does,
1158
+ # so only a min_count asking for more is applied here. The reduction the
1159
+ # member is made of is the block (super).
1160
+ private def reduce_counted (op, args, kw)
1161
+ min_count = kw.delete(:min_count)
1162
+ fill_value = kw.delete(:fill_value)
1163
+ need = CArray.__present_cells_needed_per_result_cell__(op, min_count, 0)
1164
+ result = yield(*args, **kw)
1165
+ return result if need == 0 && fill_value.nil?
1166
+ if need > 0
1167
+ present = if op == :wsum || op == :wmean
1168
+ present_pair_count(args[0], kw[:axis])
1169
+ elsif ReductionCountAndFill.mask_count?(op, args)
1170
+ if kw[:axis]
1171
+ raise NotImplementedError,
1172
+ "#{self.class}##{op}(axis:, min_count:) is not implemented"
953
1173
  end
954
- short = present.lt(need)
955
- end
956
- if short
957
- (result.is_a?(Array) ? result : [result]).each { |r| r[short] = UNDEF }
958
- end
959
- CArray.__reduce_fill__(result, fill_value)
1174
+ elements
1175
+ else
1176
+ count_not_masked(**kw)
1177
+ end
1178
+ short = present.lt(need)
1179
+ end
1180
+ if short
1181
+ (result.is_a?(Array) ? result : [result]).each { |r| r[short] = UNDEF }
960
1182
  end
1183
+ CArray.__fill_undefined_result_cells__(result, fill_value)
961
1184
  end
962
1185
  end
963
1186
  prepend ReductionCountAndFill
964
1187
 
1188
+
1189
+ # lag and strip_mask(method:) give the source's values moved or held in
1190
+ # place, so the result shows the source's attributes, as CArray#lag and
1191
+ # CArray#strip_mask(method:) do.
1192
+ prepend(Module.new do
1193
+ def lag (*args, **keywords) = @value.__send__(:carry_attrs, super)
1194
+ def strip_mask (**keywords) = @value.__send__(:carry_attrs, super)
1195
+ end)
965
1196
  end
966
1197
 
967
1198