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
@@ -78,8 +78,13 @@ class CArray
78
78
  # accumulators; `nil` for pure counts (int64).
79
79
  # @return [BincountND]
80
80
  def initialize (lengths:, fiber_shape: [], weights_data_type: nil)
81
- @lengths = lengths.map(&:to_i)
82
- raise ArgumentError, "lengths must be a non-empty list" if @lengths.empty?
81
+ raise ArgumentError, "lengths must be a non-empty list" if lengths.empty?
82
+ lengths.each_with_index do |l, k|
83
+ unless l.is_a?(Integer)
84
+ raise TypeError, "lengths[#{k}] must be an Integer (got #{l.inspect})"
85
+ end
86
+ end
87
+ @lengths = lengths.dup
83
88
  @lengths.each_with_index do |l, k|
84
89
  raise ArgumentError, "lengths[#{k}] must be >= 1" if l < 1
85
90
  end
@@ -209,6 +214,7 @@ class CArray
209
214
 
210
215
  if weights
211
216
  raise ArgumentError, "weights given but accumulator is unweighted" unless @weighted
217
+ # coerce: the accumulator's type, declared by weights_data_type:
212
218
  weights = CArray.wrap_readonly(weights, @counts_data_type)
213
219
  expected_w_shape = chunk.shape.dup
214
220
  expected_w_shape.delete_at(channel_ax)
@@ -344,6 +350,10 @@ class CArray
344
350
  # @return [BincountND]
345
351
  def bincount_nd (lengths:, axis: [-2, -1], weights: nil)
346
352
  raise ArgumentError, "lengths must be an Array of per-dim extents" unless lengths.is_a?(Array)
353
+ axis = [-2, -1] if axis.nil?
354
+ unless axis.is_a?(Array) && axis.size == 2
355
+ raise ArgumentError, "bincount_nd: axis: is a [sample, channel] pair (got #{axis.inspect})"
356
+ end
347
357
  sample_ax = normalize_axis(axis[0], "bincount_nd sample axis")
348
358
  channel_ax = normalize_axis(axis[1], "bincount_nd channel axis")
349
359
  fiber_shape = shape.dup
@@ -302,28 +302,47 @@ class CABlockIterator < CAIterator
302
302
  # @param fill_value [Object, nil] value to put in place of a masked
303
303
  # result instead of leaving it masked.
304
304
  # @return [CArray] tile-grid shaped
305
- [:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev,
306
- :variancep, :stddevp].each do |op|
307
- define_method(op) do |min_count: nil, fill_value: nil|
308
- if FACE_WINNER_INDEX.key?(op) && @source.face?
309
- return face_winner(FACE_WINNER_INDEX[op], min_count, fill_value)
310
- end
311
- kw = {}
312
- kw[:min_count] = min_count unless min_count.nil?
313
- kw[:fill_value] = fill_value unless fill_value.nil?
314
- fold(op, **kw)
305
+ CArray.send(:define_named_methods, self,
306
+ [:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev,
307
+ :variancep, :stddevp], :fold_reduction)
308
+
309
+ private def fold_reduction (op, min_count: nil, fill_value: nil)
310
+ if FACE_WINNER_INDEX.key?(op) && @source.face?
311
+ return face_winner(FACE_WINNER_INDEX[op], min_count, fill_value)
315
312
  end
313
+ kw = {}
314
+ kw[:min_count] = min_count unless min_count.nil?
315
+ kw[:fill_value] = fill_value unless fill_value.nil?
316
+ fold(op, **kw)
316
317
  end
317
318
 
318
- [:all, :any].each do |op|
319
- define_method(op) { |**kw| fold(op, **CAIterator.reduce_keywords(op, kw)) }
319
+ CArray.send(:define_named_methods, self, [:all, :any], :fold_with_keywords)
320
+
321
+ private def fold_with_keywords (op, **kw)
322
+ fold(op, **CAIterator.reduce_keywords(op, kw))
320
323
  end
321
324
 
325
+ # @!method first
326
+ # Per-tile first present value, in row-major order within the tile;
327
+ # UNDEF for a tile with none.
328
+ # @return [CArray] tile-grid shaped
329
+ # @!method last
330
+ # Per-tile last present value; UNDEF for a tile with none.
331
+ # @return [CArray] tile-grid shaped
332
+ # @!method nunique
333
+ # Per-tile number of distinct present values (int64); 0 for a tile with
334
+ # none.
335
+ # @return [CArray] tile-grid shaped
336
+ #
337
+ # These take no `min_count:` or `fill_value:`, as the core ones do not.
338
+ CArray.send(:define_named_methods, self, [:first, :last, :nunique], :fold_with_keywords)
339
+
322
340
  # A Face's minimum and maximum are cells of the Face, so they are gathered
323
341
  # from the source by the winner's address. Assembling the per-region results
324
342
  # instead copies the Face's storage into an array that no longer knows it is
325
343
  # one: a CATime came back as 8-byte strings.
326
344
  FACE_WINNER_INDEX = { min: :min_index, max: :max_index }.freeze
345
+ Ractor.make_shareable(FACE_WINNER_INDEX)
327
346
  private_constant :FACE_WINNER_INDEX
328
347
 
329
348
  # ---- count family and elements ----------------------------------------
@@ -362,11 +381,11 @@ class CABlockIterator < CAIterator
362
381
  # @return [CArray]
363
382
  def count_masked (**kw)
364
383
  kw = CAIterator.reduce_keywords(:count_masked, kw)
365
- need = CArray.__min_count_need__(:count_masked, kw[:min_count], 0)
384
+ need = CArray.__present_cells_needed_per_result_cell__(:count_masked, kw[:min_count], 0)
366
385
  out = elements - count_not_masked
367
386
  # A count of the mask reads every cell of the tile.
368
387
  out[elements.lt(need)] = UNDEF if need > 0
369
- CArray.__reduce_fill__(out, kw[:fill_value])
388
+ CArray.__fill_undefined_result_cells__(out, kw[:fill_value])
370
389
  end
371
390
 
372
391
  # @overload elements
@@ -434,13 +453,14 @@ class CABlockIterator < CAIterator
434
453
  # and with `min_count` a tile with too few present cells, is a masked cell.
435
454
  def face_winner (idx_op, min_count, fill_value)
436
455
  addr = winner_addr(idx_op)
437
- need = CArray.__min_count_need__(idx_op == :min_index ? :min : :max, min_count, 0)
456
+ need = CArray.__present_cells_needed_per_result_cell__(idx_op == :min_index ? :min : :max, min_count, 0)
438
457
  addr[count_not_masked.lt(need)] = UNDEF if need > 0
439
458
  src = @source.reshape(@source.elements)
440
459
  empty = addr.is_masked
441
460
  return src[addr].copy unless empty.any
442
- # A masked index drops the cell rather than masking it, so an empty tile
443
- # needs a placeholder address. Pointing it at a masked cell of the source
461
+ # A masked index would read as masked, but gathering by one gives the
462
+ # source a mask, so an empty tile takes a placeholder address instead.
463
+ # Pointing it at a masked cell of the source
444
464
  # carries the mask through the gather, which a read-only Face (whose copy
445
465
  # cannot be written) needs. Otherwise mask the placeholder afterwards.
446
466
  if fill_value.nil? && src.has_mask? && (hole = src.is_masked.where).elements > 0
@@ -474,7 +494,7 @@ class CABlockIterator < CAIterator
474
494
  out ||= CArray.int64(*@shape)
475
495
  out[*out_ranges] = picked
476
496
  end
477
- CArray.__reduce_fill__(out || CArray.int64(*@shape), kw[:fill_value])
497
+ CArray.__fill_undefined_result_cells__(out || CArray.int64(*@shape), kw[:fill_value])
478
498
  end
479
499
 
480
500
  public
@@ -691,24 +711,81 @@ class CABlockIterator < CAIterator
691
711
  # @!method cumcount
692
712
  # Per-tile running count of present cells (int64), source-shaped.
693
713
  # @return [CArray]
694
- [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op|
695
- define_method(op) { block_scan(op) }
714
+ CArray.send(:define_named_methods, self,
715
+ [:cumsum, :cumprod, :cummax, :cummin, :cumcount], :scan_tiles)
716
+
717
+ # A scan takes no arguments here.
718
+ private def scan_tiles (op, *args)
719
+ unless args.empty?
720
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)"
721
+ end
722
+ block_scan(op)
723
+ end
724
+
725
+ # @overload lag(n = 1)
726
+ # Per-tile value `n` steps earlier, counting the tile's cells row-major;
727
+ # UNDEF where the tile has none, so a lag never reaches into another
728
+ # tile. A partial edge tile counts only the cells it has.
729
+ # @return [CArray] source-shaped
730
+ def lag (n = 1)
731
+ unless n.is_a?(Integer)
732
+ raise TypeError, "lag: n must be an Integer (got #{n.class})"
733
+ end
734
+ tile_wise { |rows, axis| rows.lag(n, axis: axis) }
735
+ end
736
+
737
+ # @overload strip_mask(method:)
738
+ # Per-tile fill of the masked cells, counting the tile's cells row-major:
739
+ # `:forward` / `:ffill` or `:backward` / `:bfill`. A fill never reaches
740
+ # into another tile, so a masked cell with nothing to carry in its tile
741
+ # stays masked.
742
+ # @return [CArray] source-shaped
743
+ def strip_mask (method:)
744
+ CAIterator.hold_backward?(method)
745
+ tile_wise { |rows, axis| rows.strip_mask(method: method, axis: axis) }
746
+ end
747
+
748
+ # @overload order(descending: false, method: :ordinal, kind: :quick)
749
+ # Per-tile rank of each cell among the tile's cells (int64, `0` the
750
+ # smallest), as `CArray#order` ranks them; a partial edge tile ranks only
751
+ # the cells it has. The keywords are those of `CArray#order`.
752
+ # @return [CArray] source-shaped
753
+ def order (**kw)
754
+ tile_wise { |rows, axis| rows.order(axis: axis, **kw) }
696
755
  end
697
756
 
698
757
  private
699
758
 
759
+ # A source-shaped result built tile by tile: the block gets a region's
760
+ # tiles as rows (one tile's cells, row-major, along +axis+) and gives back
761
+ # rows of the same shape. Each region has whole tiles of one size, so a
762
+ # partial edge tile is its own cells rather than cells and padding.
763
+ def tile_wise
764
+ out = nil
765
+ each_region do |strip_ranges, tiles, _out_ranges|
766
+ view = @source[*strip_ranges].block_view(*tiles)
767
+ grid = view.shape[0, @sndim]
768
+ found = yield(view.copy.reshape(*grid, tiles.inject(1, :*)), @sndim)
769
+ .reshape(*grid, *tiles)
770
+ out ||= tile_output(found)
771
+ out[*strip_ranges].block_view(*tiles)[] = tile_values(out, found)
772
+ end
773
+ # a source with no cells has no region; its one-cell rows give the type
774
+ out || yield(@source.copy.reshape(*@source.shape, 1), @sndim).reshape(*@source.shape)
775
+ end
776
+
700
777
  # Drive a within-tile segment scan: scan each tile flattened row-major and
701
778
  # scatter back to a source-shaped result. The output data type is taken from the
702
779
  # first tile's scan (all tiles share it); the padded margin is UNDEF and the
703
780
  # OOB cells of a partial edge tile are cropped from the result.
704
- def block_scan (op)
781
+ def block_scan (op, *args)
705
782
  tgv = tile_grid_view
706
783
  nils = Array.new(@sndim, nil)
707
784
  pout = nil
708
785
  pgv = nil
709
786
  CArray.each_index(*@shape) do |*g|
710
787
  tile = tgv[*g, *nils]
711
- scanned = tile.copy.reshape(tile.elements).public_send(op).reshape(*@sizes)
788
+ scanned = tile.copy.reshape(tile.elements).public_send(op, *args).reshape(*@sizes)
712
789
  unless pout
713
790
  pout = CArray.new(scanned.data_type, padded_source.shape, bytes: scanned.bytes)
714
791
  pout[] = UNDEF
@@ -719,6 +796,24 @@ class CABlockIterator < CAIterator
719
796
  pout[*@sndim.times.map { |i| 0...@source.shape[i] }].copy
720
797
  end
721
798
 
799
+ # A source-shaped array that can hold what #tile_wise gives: of its kind,
800
+ # or an object array for a read-only Face, which cannot be written cell by
801
+ # cell.
802
+ def tile_output (lagged)
803
+ if lagged.face? && lagged.read_only?
804
+ CArray.object(*@source.shape)
805
+ else
806
+ lagged.__send__(:blank_like, @source.shape)
807
+ end
808
+ end
809
+
810
+ # The values as +out+ can hold them: a Face going into an object array
811
+ # goes as its surface values, not its storage.
812
+ def tile_values (out, lagged)
813
+ return lagged unless out.data_type == CA_OBJECT && lagged.face?
814
+ CArray.object(*lagged.shape) { lagged.to_a }
815
+ end
816
+
722
817
  # A padded entity whose interior holds the source and whose partial-edge
723
818
  # margin is masked (UNDEF), sized to exactly cover the ceil tile grid. When
724
819
  # the source already divides evenly this is the source itself (no copy), so
@@ -729,7 +824,7 @@ class CABlockIterator < CAIterator
729
824
  if pshape == @source.shape.to_a
730
825
  @source
731
826
  else
732
- pad = CArray.new(@source.data_type, pshape, bytes: @source.bytes)
827
+ pad = @source.__send__(:blank_like, pshape) # a Face stays one
733
828
  pad[] = UNDEF
734
829
  pad[*@sndim.times.map { |i| 0...@source.shape[i] }] = @source
735
830
  pad
@@ -742,6 +837,14 @@ class CABlockIterator < CAIterator
742
837
  def tile_grid_view
743
838
  @tile_grid_view ||= padded_source.block_view(*@sizes)
744
839
  end
840
+
841
+ # lag and strip_mask(method:) give the source's values moved or held in
842
+ # place, so the result shows the source's attributes, as CArray#lag and
843
+ # CArray#strip_mask(method:) do.
844
+ prepend(Module.new do
845
+ def lag (*args, **keywords) = @source.__send__(:carry_attrs, super)
846
+ def strip_mask (**keywords) = @source.__send__(:carry_attrs, super)
847
+ end)
745
848
  end
746
849
 
747
850
 
@@ -1,6 +1,6 @@
1
1
  # Boolean folds all / any / none gain a `skip_masked:` keyword.
2
2
  #
3
- # skip_masked: true (default) -- available-case (skipna): masked cells are
3
+ # skip_masked: true (default) -- available-case: masked cells are
4
4
  # ignored, the result is always true/false.
5
5
  # This is the existing behavior; every other
6
6
  # keyword (axis:, keep_axis:, ...) forwards
@@ -14,15 +14,15 @@
14
14
  # false < unknown < true).
15
15
  #
16
16
  # The Kleene fold composes from count(true) / count(false) / count_masked, so
17
- # there is no dedicated kernel; the C reductions keep the skipna semantics
18
- # under the `__*_skipna__` aliases.
17
+ # there is no dedicated kernel; the C reductions that skip masked cells stay
18
+ # reachable as the private `*_skipping_masked` aliases.
19
19
 
20
20
  class CArray
21
21
 
22
- alias_method :__any_skipna__, :any
23
- alias_method :__all_skipna__, :all
24
- alias_method :__none_skipna__, :none
25
- private :__any_skipna__, :__all_skipna__, :__none_skipna__
22
+ alias_method :any_skipping_masked, :any
23
+ alias_method :all_skipping_masked, :all
24
+ alias_method :none_skipping_masked, :none
25
+ private :any_skipping_masked, :all_skipping_masked, :none_skipping_masked
26
26
 
27
27
  # Whether any cell is true.
28
28
  #
@@ -36,8 +36,8 @@ class CArray
36
36
  # `keep_axis:`, ...).
37
37
  # @return [Boolean, CArray] a scalar, or an array when an axis is given.
38
38
  def any (skip_masked: true, **opts)
39
- return __any_skipna__(**opts) if skip_masked
40
- __kleene_fold(:any, opts)
39
+ return any_skipping_masked(**opts) if skip_masked
40
+ kleene_fold(:any, opts)
41
41
  end
42
42
 
43
43
  # Whether every cell is true.
@@ -52,8 +52,8 @@ class CArray
52
52
  # `keep_axis:`, ...).
53
53
  # @return [Boolean, CArray] a scalar, or an array when an axis is given.
54
54
  def all (skip_masked: true, **opts)
55
- return __all_skipna__(**opts) if skip_masked
56
- __kleene_fold(:all, opts)
55
+ return all_skipping_masked(**opts) if skip_masked
56
+ kleene_fold(:all, opts)
57
57
  end
58
58
 
59
59
  # Whether no cell is true.
@@ -68,8 +68,8 @@ class CArray
68
68
  # `keep_axis:`, ...).
69
69
  # @return [Boolean, CArray] a scalar, or an array when an axis is given.
70
70
  def none (skip_masked: true, **opts)
71
- return __none_skipna__(**opts) if skip_masked
72
- __kleene_fold(:none, opts)
71
+ return none_skipping_masked(**opts) if skip_masked
72
+ kleene_fold(:none, opts)
73
73
  end
74
74
 
75
75
  private
@@ -79,11 +79,11 @@ class CArray
79
79
  # any, a false for all, a true for none); elsewhere a masked cell leaves the
80
80
  # answer undetermined. Going through that reduction checks the data type,
81
81
  # the axes and the keywords exactly as `skip_masked: true` does.
82
- def __kleene_fold (kind, opts)
82
+ def kleene_fold (kind, opts)
83
83
  r = case kind
84
- when :any then __any_skipna__(**opts)
85
- when :all then __all_skipna__(**opts)
86
- else __none_skipna__(**opts)
84
+ when :any then any_skipping_masked(**opts)
85
+ when :all then all_skipping_masked(**opts)
86
+ else none_skipping_masked(**opts)
87
87
  end
88
88
  cm = count_masked(**opts)
89
89
  if r.is_a?(CArray)
@@ -48,6 +48,7 @@
48
48
  # ----------------------------------------------------------------------------
49
49
 
50
50
  require "carray"
51
+ require "carray/methods/is_in" # refuse_range_set, for the set-valued verbs
51
52
 
52
53
  # Categorical column: dense integer codes plus a label vocabulary, so each
53
54
  # element is an index into a small set of categories. Structurally the same
@@ -68,6 +69,7 @@ class CACategorical < CAObject
68
69
  CA_INT32 => "l", CA_UINT32 => "L",
69
70
  CA_INT64 => "q", CA_UINT64 => "Q",
70
71
  }.freeze
72
+ Ractor.make_shareable(UNPACK_FORMAT)
71
73
 
72
74
  # The exclusion sentinel per codes data_type: the all-ones bit pattern, read
73
75
  # as type-max for an unsigned data type and as -1 for a signed one. Either way it
@@ -214,6 +216,34 @@ class CACategorical < CAObject
214
216
  end
215
217
  end
216
218
 
219
+ # The rows' categorical for key columns of one length (CAFrame#group_by,
220
+ # #is_duplicate, windows(on:, by:)). Each column is categorized on its
221
+ # own, so a component compares as a single key does (NaN is one value,
222
+ # -0.0 is 0.0, a masked cell matches nothing); with several columns a
223
+ # row's category is the tuple of its component codes, labelled by the
224
+ # tuple of the component labels. One masked component leaves the row in
225
+ # no category, as a masked single key does.
226
+ private_class_method def self.from_keys (cols)
227
+ return cols.first.categorize if cols.size == 1
228
+ nrow = cols.first.elements
229
+ cats = cols.map(&:categorize)
230
+ sizes = cats.map { |c| c.labels.size }
231
+ tuple = if sizes.inject(1, :*) <= 2**62
232
+ cats.drop(1).inject(cats[0].codes.int64) { |acc, c| acc * c.labels.size + c.codes.int64 }
233
+ else
234
+ codes = cats.map(&:codes)
235
+ t = CArray.object(nrow) { |i| codes.map { |c| c[i] } }
236
+ nrow.times { |i| t[i] = UNDEF if t[i].any? { |x| UNDEF.equal?(x) } }
237
+ t
238
+ end
239
+ rows = tuple.categorize
240
+ labels = rows.labels.map do |code|
241
+ parts = code.is_a?(Array) ? code : sizes.reverse.map { |n| code, d = code.divmod(n); d }.reverse
242
+ parts.each_with_index.map { |d, j| cats[j].labels[d] }
243
+ end
244
+ from_codes(rows.codes, labels)
245
+ end
246
+
217
247
  # codes : integer CArray, the storage parent, already normalised by the
218
248
  # caller (categorize builds it that way; from_codes rewrites what it
219
249
  # receives). Excluded cells are both masked AND store the type-max
@@ -235,7 +265,7 @@ class CACategorical < CAObject
235
265
  # copy first so a caller's array is never frozen as a side effect; the
236
266
  # label objects themselves are left untouched (container-level freeze).
237
267
  @labels = (labels.respond_to?(:to_a) ? labels.to_a : Array(labels)).dup.freeze
238
- super(CA_FIXLEN, codes.dim,
268
+ super(CA_FIXLEN, codes.shape,
239
269
  bytes: codes.bytes,
240
270
  storage: codes.data_type,
241
271
  parent: codes,
@@ -309,14 +339,6 @@ class CACategorical < CAObject
309
339
  # the integer code. The raw codes stay reachable via #codes for code-space
310
340
  # work (interop, ML features), but they are not the everyday surface.
311
341
 
312
- # Boolean mask of cells whose category == label. Excluded cells stay UNDEF
313
- # (their category is unknown); an unknown label yields an all-false mask.
314
- # @overload eq(label)
315
- # Returns a boolean CArray marking cells whose category equals
316
- # `label`. Excluded cells stay masked; an unknown label yields
317
- # an all-false result.
318
- # @param label [Object] category to match.
319
- # @return [CArray]
320
342
  # @overload resize(*newdim, fill_value: UNDEF)
321
343
  # Returns a categorical resized to `newdim` (see {CArray#resize}).
322
344
  # The new cells are excluded (UNDEF) unless `fill_value` names one of
@@ -338,16 +360,28 @@ class CACategorical < CAObject
338
360
  CACategorical.from_codes(codes.insert_block(offset, bsize) { fill }, labels)
339
361
  end
340
362
 
363
+ # Boolean mask of cells whose category == label. Excluded cells stay UNDEF
364
+ # (their category is unknown); an unknown label yields an all-false mask.
365
+ # @overload eq(label)
366
+ # Returns a boolean CArray marking cells whose category equals
367
+ # `label`. A CArray is compared cell by cell, by label: another
368
+ # categorical is matched whatever order its labels are in, and a
369
+ # masked cell on either side gives UNDEF. Anything else, a Ruby Array
370
+ # included, is one label, as for {CArray#eq}. An unknown label matches
371
+ # nothing.
372
+ # @param label [Object, CArray] category to match, or one per cell.
373
+ # @return [CArray]
341
374
  def eq(label)
342
- codes.eq(@labels.index(label) || @labels.size)
375
+ codes.eq(codes_of(label))
343
376
  end
344
377
 
345
378
  # @overload ne(label)
346
379
  # Returns the complement of {#eq}.
347
- # @param label [Object] category to compare against.
380
+ # @param label [Object, CArray] category to compare against, or one per
381
+ # cell.
348
382
  # @return [CArray]
349
383
  def ne(label)
350
- codes.ne(@labels.index(label) || @labels.size)
384
+ codes.ne(codes_of(label))
351
385
  end
352
386
 
353
387
  # @overload count(label)
@@ -446,6 +480,7 @@ class CACategorical < CAObject
446
480
  # @param values [Array, CArray, CACategorical] labels to test against.
447
481
  # @return [CArray]
448
482
  def is_in (values)
483
+ refuse_range_set(values, "is_in")
449
484
  wanted = label_list(values).filter_map { |l| @labels.index(l) }
450
485
  codes.is_in(CArray.int64(wanted.size) { |i| wanted[i] })
451
486
  end
@@ -453,18 +488,21 @@ class CACategorical < CAObject
453
488
  # @overload intersection(other, sort: false)
454
489
  # @return [CArray] object CArray of the labels present in both.
455
490
  def intersection (other, sort: false)
491
+ refuse_range_set(other, "intersection")
456
492
  unique.intersection(label_array(other), sort: sort)
457
493
  end
458
494
 
459
495
  # @overload difference(other, sort: false)
460
496
  # @return [CArray] object CArray of the labels only `self` has.
461
497
  def difference (other, sort: false)
498
+ refuse_range_set(other, "difference")
462
499
  unique.difference(label_array(other), sort: sort)
463
500
  end
464
501
 
465
502
  # @overload union(other, sort: false)
466
503
  # @return [CArray] object CArray of the labels of either side.
467
504
  def union (other, sort: false)
505
+ refuse_range_set(other, "union")
468
506
  unique.union(label_array(other), sort: sort)
469
507
  end
470
508
 
@@ -484,6 +522,24 @@ class CACategorical < CAObject
484
522
  # that has none. Every code-to-label path goes through here rather than
485
523
  # indexing @labels itself, because two decodes are two chances to disagree,
486
524
  # and disagreeing is how #unique came to contradict #to_a on the same cell.
525
+ # An operand of #eq / #ne as codes of this vocabulary. A value that is not
526
+ # one of the labels gets labels.size, which no cell holds. A CArray is read
527
+ # cell by cell, a masked cell staying masked; a categorical goes through its
528
+ # own codes, so the two vocabularies need not agree. Anything else is one
529
+ # label, as CArray#eq takes it.
530
+ def codes_of (other)
531
+ return @labels.index(other) || @labels.size unless other.is_a?(CArray)
532
+ if other.is_a?(CACategorical)
533
+ table = other.labels.map { |l| @labels.index(l) || @labels.size }
534
+ return CA_INT64(table)[other.codes] unless table.empty?
535
+ end
536
+ memo = {}
537
+ list = other.flatten.to_a.map { |v|
538
+ UNDEF.equal?(v) ? v : memo.fetch(v) { memo[v] = @labels.index(v) || @labels.size }
539
+ }
540
+ CA_INT64(list).reshape(*other.shape)
541
+ end
542
+
487
543
  # The code a fill value stands for: UNDEF stays UNDEF, a label is its code.
488
544
  def code_for_fill (value)
489
545
  return UNDEF if UNDEF.equal?(value)
@@ -564,35 +620,17 @@ class CACategorical < CAObject
564
620
  # `value.reshape(elements)[cat.sort_addr]` produces the contiguous blocks.
565
621
  # Memoised (see the grouping-plan note above): the counting sort is the
566
622
  # dominant grouping cost, so it is computed once and shared by every payload
567
- # column and by CACategoricalIterator. Do not mutate the returned array.
623
+ # column and by CACategoricalIterator, and kept for the categorical's
624
+ # lifetime (an int64 per cell). Do not mutate the returned array.
568
625
  # @overload sort_addr
569
626
  # Returns a length-{#elements} integer CArray of flat storage
570
627
  # addresses that order the cells by category (excluded cells last).
571
628
  # @return [CArray]
572
629
  def sort_addr
573
630
  return @sort_addr if @sort_addr
574
- n = elements
575
- k = @labels.size
576
- nvalid = category_sizes.sum
577
- cur = reduceat_index.copy # segment starts, consumed as cursor
578
- flat = codes.reshape(n)
579
- seq = CArray.int64(n).seq! # source indices, scattered as payload
580
- out = CArray.int64(n)
581
- if nvalid == n
582
- flat.send(:__categorical_scatter__, seq, cur, out, k) # all valid: scatter straight in
583
- else
584
- if nvalid > 0
585
- valid = CArray.int64(nvalid) # scatter target must be an entity, not a view
586
- flat.send(:__categorical_scatter__, seq, cur, valid, k)
587
- out[0...nvalid] = valid
588
- end
589
- # Excluded = every cell the scatter skips: code out of [0, k), OR masked
590
- # (a from_codes pre-masked cell keeps a valid code but is excluded). Both
591
- # must be caught or the tail slot count would not add up to n - nvalid.
592
- excluded = flat.value.ge(k).or(flat.value.lt(0))
593
- excluded = excluded.or(flat.is_masked) if flat.has_mask?
594
- out[nvalid..-1] = seq[excluded] # excluded cells, source order
595
- end
631
+ out = codes.reshape(elements).__send__(:addresses_by_category,
632
+ reduceat_index.copy, @labels.size,
633
+ category_sizes.sum)
596
634
  @sort_addr = out
597
635
  out
598
636
  end
@@ -630,6 +668,75 @@ class CACategorical < CAObject
630
668
  CACategorical.from_codes(recoded.codes, recoded.labels.map { |c| label_at(c) })
631
669
  end
632
670
 
671
+ # The position of +label+ among the labels, found by ==, a NaN label by a
672
+ # NaN, as join and at match NaN keys; nil when it is not one of them.
673
+ private def label_position (label)
674
+ return @labels.index(label) unless label.is_a?(Float) && label.nan?
675
+ @labels.index { |l| l.is_a?(Float) && l.nan? }
676
+ end
677
+
678
+ # The categorical as numeric columns, one per category, for a model that
679
+ # takes numbers: row i holds 1 in the column of its category and 0
680
+ # elsewhere. With an intercept in the model the k columns would sum to the
681
+ # intercept's column, so by default the column of the base category is
682
+ # left out and k - 1 remain; each coefficient then reads as that category
683
+ # minus the base. +full: true+ keeps all k (a model without an intercept,
684
+ # or an input to a learner).
685
+ #
686
+ # The columns follow the labels, not the data: a category with no cells
687
+ # gives a column of zeros, so two categoricals with the same labels give
688
+ # the same columns. A masked cell gives a masked row.
689
+ #
690
+ # @overload contrast(scheme = :dummy, base: nil, full: false)
691
+ # Returns an int8 CArray of shape (n, k - 1), or (n, k) with
692
+ # `full: true`, whose columns are the labels in order with the base
693
+ # left out.
694
+ # @param scheme [Symbol] the coding; only `:dummy` is defined.
695
+ # @param base [Object] the label whose column is left out; the first
696
+ # label when omitted.
697
+ # @param full [Boolean] keep every column.
698
+ # @return [CArray]
699
+ # @raise [ArgumentError] when `scheme` is not `:dummy`, when `base` is
700
+ # not a label, when `base:` and `full: true` are both given, when
701
+ # there are no labels to leave one out of, or when the categorical is
702
+ # not one-dimensional.
703
+ def contrast (scheme = :dummy, base: nil, full: false)
704
+ unless scheme == :dummy
705
+ raise ArgumentError, "contrast: unknown scheme #{scheme.inspect} (only :dummy is defined)"
706
+ end
707
+ unless ndim == 1
708
+ raise ArgumentError, "contrast: the categorical is #{ndim}-dimensional, not one-dimensional"
709
+ end
710
+ k = @labels.size
711
+ if full
712
+ raise ArgumentError, "contrast: base: and full: true contradict (full keeps every column)" unless base.nil?
713
+ kept = (0...k).to_a
714
+ else
715
+ raise ArgumentError, "contrast: there are no labels, so no base column to leave out" if k == 0
716
+ dropped = base.nil? ? 0 : label_position(base)
717
+ raise ArgumentError, "contrast: #{base.inspect} is not one of the labels #{@labels.inspect}" unless dropped
718
+ kept = (0...k).to_a - [dropped]
719
+ end
720
+ n = elements
721
+ m = kept.size
722
+ out = CArray.int8(n, m).zero
723
+ return out if n == 0 || m == 0
724
+
725
+ # One pass: each cell's code picks its column through colmap (-1 for the
726
+ # base), and a 1 is written at row * m + column.
727
+ colmap = CArray.int64(k) { -1 }
728
+ kept.each_with_index { |code, j| colmap[code] = j }
729
+ excluded = codes.has_mask? ? codes.is_masked : CArray.boolean(n) { false }
730
+ code = codes.value.to_type(CA_INT64)
731
+ code[excluded] = 0 if excluded.any
732
+ col = colmap[code]
733
+ hit = col.ge(0) & excluded.not
734
+ addr = CArray.int64(n).seq! * m + col
735
+ out.reshape(n * m)[addr[hit]] = 1 if hit.any
736
+ out[excluded, nil] = UNDEF if excluded.any
737
+ out
738
+ end
739
+
633
740
  # @overload inspect
634
741
  # Returns a short summary showing element count, category count,
635
742
  # and vocabulary.
@@ -667,14 +774,11 @@ class CArray
667
774
  # duplicates.
668
775
  # @raise [CArray::DataTypeError] when `sort_labels: true` discovers
669
776
  # the labels of a complex array, which have no order.
670
- # The order sort_labels: puts two labels in: as sort orders the cells of an
671
- # array, a Float NaN after every other value; two labels <=> cannot order
672
- # are refused, by name.
777
+ # The order sort_labels: puts two labels in: the order sort puts the cells
778
+ # of an array in (object_order_cmp); two labels <=> cannot order are
779
+ # refused, by name.
673
780
  private def label_order(a, b)
674
- na = a.is_a?(Float) && a.nan?
675
- nb = b.is_a?(Float) && b.nan?
676
- return (na ? 1 : 0) - (nb ? 1 : 0) if na || nb
677
- c = a <=> b
781
+ c = CArray.send(:object_order_cmp, a, b)
678
782
  if c.nil?
679
783
  raise ArgumentError,
680
784
  "categorize(sort_labels: true): cannot order the labels " \
@@ -706,7 +810,13 @@ class CArray
706
810
  # Sort the k levels as Ruby sorts them, then renumber every code through
707
811
  # the rank in one gather. The sentinel of an excluded cell is out of the
708
812
  # rank's range, so project makes it UNDEF and from_codes writes it back.
709
- order = (0...levels.size).sort { |i, j| label_order(levels[i], levels[j]) }
813
+ # A boolean label reads as true or false, and sorts as its storage,
814
+ # false before true, as sort and unique(sort: true) put the cells.
815
+ order = if boolean?
816
+ (0...levels.size).sort_by { |i| levels[i] ? 1 : 0 }
817
+ else
818
+ (0...levels.size).sort { |i, j| label_order(levels[i], levels[j]) }
819
+ end
710
820
  rank = CArray.new(codes.data_type, [levels.size])
711
821
  order.each_with_index { |old, new_code| rank[old] = new_code }
712
822
  return CACategorical.from_codes(rank.project(codes), order.map { |i| levels[i] })
@@ -744,3 +854,15 @@ class CArray
744
854
  CACategorical.from_codes(codes, labels_arr)
745
855
  end
746
856
  end
857
+
858
+ # categorize is a conversion: the categories are the values in another
859
+ # representation, so they show the attributes the values show. Each of the
860
+ # three implementations returns from several places, so the attributes are
861
+ # carried here, around all of them.
862
+ [CArray, CACategorical, CAConstString].each do |klass|
863
+ klass.prepend(Module.new do
864
+ def categorize (labels: nil, sort_labels: false)
865
+ carry_attrs(super)
866
+ end
867
+ end)
868
+ end