carray 3.0.3 → 3.0.5

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