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
@@ -63,11 +63,13 @@ require "carray"
63
63
  require "carray/segment_iterator"
64
64
 
65
65
  # A CAIterator over the categories of a CACategorical. It is a
66
- # CASegmentIterator over a category-sorted copy of the value: construction
67
- # gathers the value through the categorical's cached grouping plan, so each
68
- # category is one contiguous segment, and every reduction without `axis:` is
69
- # the segment reduction over that copy (one value per category, in {#labels}
70
- # order). This class adds the construction, the labels, and the per-fiber
66
+ # CASegmentIterator over a category-sorted copy of the value: the value is
67
+ # gathered through the categorical's cached grouping plan, so each category
68
+ # is one contiguous segment, and every reduction without `axis:` is the
69
+ # segment reduction over that copy (one value per category, in {#labels}
70
+ # order). With few categories and a numeric value, the count / sum / min /
71
+ # max are read from the values by code instead, and the copy is gathered
72
+ # only for the reductions that need it. This class adds the construction, the labels, and the per-fiber
71
73
  # form (`axis:`), which classifies each fiber along an axis and keeps the
72
74
  # other axes.
73
75
  class CACategoricalIterator < CASegmentIterator
@@ -106,10 +108,9 @@ class CACategoricalIterator < CASegmentIterator
106
108
  nvalid = counts.sum
107
109
  @offsets = cat.reduceat_index # cached segment STARTS (int64[k])
108
110
  @bounds = CArray.segment_offsets(lengths: @elements) # every boundary (int64[k+1])
109
- # Group-major source indices = the valid prefix of the cached sort_addr.
110
- # With no classified cell the prefix is empty (and slicing a length-0
111
- # sort_addr would be out of range), so take the empty permutation directly.
112
- @perm = nvalid > 0 ? cat.sort_addr[0...nvalid] : CArray.int64(0)
111
+ # The group-major source indices (#perm) are taken from the cached
112
+ # sort_addr when first asked for.
113
+ @nvalid = nvalid
113
114
  @codes = cat.codes.reshape(cat.elements) # flat codes view (map re-walk / weights)
114
115
  # Gather value into category-contiguous order via the cached permutation
115
116
  # and materialise (the reduceat kernels read the grouped buffer's raw ptr,
@@ -118,9 +119,21 @@ class CACategoricalIterator < CASegmentIterator
118
119
  # rebuilt per column. With no classified cell (empty / all-excluded) there
119
120
  # is nothing to gather — an empty index into an empty source is out of
120
121
  # range — so build the empty grouped buffer directly.
121
- @grouped = nvalid > 0 ? value.reshape(value.elements)[@perm].copy
122
- : CArray.new(value.data_type, [0], bytes: value.bytes)
123
- @empty = CArray.new(grouped.data_type, [0], bytes: grouped.bytes)
122
+ #
123
+ # With few categories, a numeric value is instead copied as it lies:
124
+ # the count / sum / min / max read it in one pass by code (scattering
125
+ # into one slot per category beats gathering every cell first while
126
+ # the slots stay in cache), and the grouped copy is gathered from it
127
+ # only when a reduction needs the categories contiguous. Either way
128
+ # the iterator holds the values as they were when it was built.
129
+ flat = value.reshape(value.elements)
130
+ if @k <= SCATTER_CATEGORIES_MAX && kernel_reads?(value.data_type)
131
+ @as_built = flat.copy
132
+ @empty = @as_built[0...0]
133
+ else
134
+ @grouped = nvalid > 0 ? flat[perm].copy : flat[0...0].copy
135
+ @empty = @grouped[0...0] # an empty category is the values' own kind
136
+ end
124
137
  else
125
138
  # Shape mismatch: only per-fiber axis: dispatch could still work. With a
126
139
  # 1-D value there is no fiber structure to broadcast into, so a mismatch
@@ -148,6 +161,10 @@ class CACategoricalIterator < CASegmentIterator
148
161
  self
149
162
  end
150
163
 
164
+ # The most categories for which the count / sum / min / max are read by
165
+ # scattering the values by code rather than through the grouped copy.
166
+ SCATTER_CATEGORIES_MAX = 4096
167
+
151
168
  # @overload labels
152
169
  # Returns the category vocabulary the results are aligned to.
153
170
  # @return [Array]
@@ -299,7 +316,13 @@ class CACategoricalIterator < CASegmentIterator
299
316
 
300
317
  # Both are built with the grouping plan, and only on the flat path; the
301
318
  # per-fiber form has neither and must not build a segment's.
319
+ # Group-major source indices = the valid prefix of the cached sort_addr.
320
+ # With no classified cell the prefix is empty (and slicing a length-0
321
+ # sort_addr would be out of range), so take the empty permutation directly.
302
322
  def perm
323
+ if @nvalid && ! @perm
324
+ @perm = @nvalid > 0 ? @cat.sort_addr[0...@nvalid] : CArray.int64(0)
325
+ end
303
326
  @perm
304
327
  end
305
328
 
@@ -307,6 +330,34 @@ class CACategoricalIterator < CASegmentIterator
307
330
  @codes
308
331
  end
309
332
 
333
+ # With few categories the grouped copy is gathered from the values as
334
+ # built, the first time a reduction asks for it.
335
+ def grouped
336
+ if @as_built && ! @grouped
337
+ @grouped = @nvalid > 0 ? @as_built[perm].copy : @as_built[0...0].copy
338
+ end
339
+ super
340
+ end
341
+
342
+ def kernel_reads? (data_type = (@as_built || grouped).data_type)
343
+ super(data_type)
344
+ end
345
+
346
+ # With few categories the count / sum / min / max come from one pass over
347
+ # the values as built, each cell added to the slot of its code; a cell
348
+ # whose value or code is masked, or whose code names no category, is in
349
+ # no slot, as it is in no segment of the grouped copy.
350
+ def moments
351
+ return @moments if defined?(@moments)
352
+ return super unless @as_built && kernel_reads?
353
+ counts = CArray.int64(@k)
354
+ sums = CArray.float64(@k)
355
+ mins = CArray.new(@as_built.data_type, [@k])
356
+ maxs = CArray.new(@as_built.data_type, [@k])
357
+ @as_built.__send__(:__fiber_scatter_moments__, @codes, 0, @k, counts, sums, mins, maxs)
358
+ @moments = { count: counts, sum: sums, min: mins, max: maxs }
359
+ end
360
+
310
361
  # Axis-aware count of present cells. Counting how many cells fall in a
311
362
  # group does not look at what is in them. An integer or float payload goes
312
363
  # through the fused moments kernel, which counts in the same pass that
@@ -315,7 +366,7 @@ class CACategoricalIterator < CASegmentIterator
315
366
  # counted from the codes and the value's mask, one group at a time.
316
367
  def axis_counts (axis, op)
317
368
  h = @value
318
- return axis_moments(axis, op)[:count] if h.integer? || h.float?
369
+ return axis_moments(axis, op)[:count] if kernel_reads?(h.data_type)
319
370
  axis = checked_axis(axis, op)
320
371
  full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
321
372
  band = h.shape.dup; band.delete_at(axis)
@@ -349,7 +400,7 @@ class CACategoricalIterator < CASegmentIterator
349
400
  # this one thing was the odd choice; a caller who wants a fused kernel's
350
401
  # four answers shares them by keeping the result.
351
402
  def axis_moments (axis, op)
352
- return axis_moments_by_core(axis, op) unless MONOID_TYPES.include?(@value.data_type)
403
+ return axis_moments_by_core(axis, op) unless kernel_reads?(@value.data_type)
353
404
  h = @value
354
405
  axis = checked_axis(axis, op)
355
406
  band = h.shape.dup; band.delete_at(axis)
@@ -382,6 +433,7 @@ class CACategoricalIterator < CASegmentIterator
382
433
  # kernel had all four from one.
383
434
  BAND_ONLY_KEYS = { sum: [:sum], mean: [:sum, :count], min: [:min],
384
435
  max: [:max], minmax: [:min, :max] }.freeze
436
+ Ractor.make_shareable(BAND_ONLY_KEYS)
385
437
  private_constant :BAND_ONLY_KEYS
386
438
 
387
439
  def band_only_moments (h, axis, band, op)
@@ -419,7 +471,7 @@ class CACategoricalIterator < CASegmentIterator
419
471
  # Axis-aware mean: sums / counts (float64); empty group cells (count=0) MASKED.
420
472
  # Matches flat #mean per fiber.
421
473
  def axis_mean (axis)
422
- return axis_by_core(axis, :mean) unless MONOID_TYPES.include?(@value.data_type)
474
+ return axis_by_core(axis, :mean) unless kernel_reads?(@value.data_type)
423
475
  m = axis_moments(axis, :mean)
424
476
  cnt = m[:count]
425
477
  out = m[:sum] / cnt.float64 # count 0 -> NaN
@@ -448,14 +500,13 @@ class CACategoricalIterator < CASegmentIterator
448
500
  slot_idx = [nil] + [nil] * band.size # placeholder; c fills slot 0
449
501
  codes_bad = full_c.has_mask? ? full_c.is_masked : nil
450
502
  @k.times do |c|
451
- h_local = h.copy
452
503
  # Boolean of cells that DO belong to group c (with codes present). On
453
504
  # any masked codes cell the codes.eq(c) result carries UNDEF, which
454
505
  # naturally reads as "not in group c" for our exclusion purpose.
455
506
  in_c = full_c.eq(c)
456
507
  exclude = in_c.not
457
508
  exclude = exclude | codes_bad if codes_bad
458
- h_local[exclude] = UNDEF
509
+ h_local = h.mask_where(exclude)
459
510
  slice = h_local.__send__(op, axis: axis) # band shape, mask carries n<contract
460
511
  slot_idx[0] = c
461
512
  out[*slot_idx] = slice
@@ -472,16 +523,21 @@ class CACategoricalIterator < CASegmentIterator
472
523
  # taken by index; otherwise its cells are kept by masking the rest of a copy.
473
524
  # A read-only Face's answers (a const string's records index a pool of
474
525
  # their own) cannot be put together, so they are collected as the surface
475
- # values they are, as the flat form does.
526
+ # values they are, as the flat form does. Over a 1-D value each group
527
+ # reduces to one cell, kept as a 1-element array so the answers stack.
476
528
  def axis_by_core (axis, op, *args)
477
529
  h = @value
478
530
  axis = checked_axis(axis, op)
531
+ flat = (h.ndim == 1)
479
532
  groups = (0...[@k, 1].max).map { |c| group_cells(h, axis, c, @k.zero?) }
480
- answers = groups.map { |g| g.__send__(op, *args, axis: axis) }
533
+ answers = groups.map do |g|
534
+ flat ? g.__send__(op, *args, axis: axis, keep_axis: true)
535
+ : g.__send__(op, *args, axis: axis)
536
+ end
481
537
  if answers.first.face? && answers.first.read_only?
482
538
  answers = answers.map { |a| CArray.object(*a.shape) { a.to_a } }
483
539
  end
484
- out = CArray.stack(answers, axis: 0).copy
540
+ out = flat ? CArray.concatenate(answers) : CArray.stack(answers, axis: 0).copy
485
541
  @k.zero? ? out[0...0, *([nil] * (out.ndim - 1))].copy : out
486
542
  end
487
543
 
@@ -493,7 +549,9 @@ class CACategoricalIterator < CASegmentIterator
493
549
 
494
550
  def group_cells (h, axis, c, none)
495
551
  codes = @cat.codes
496
- if codes.shape == [h.shape[axis]]
552
+ # Classified along the axis, a group is taken by index; the general way,
553
+ # masking the rest of a copy, gives the same cells.
554
+ if codes.shape == [h.shape[axis]] && CArray.__fast_paths_enabled__
497
555
  sel = none ? CArray.boolean(codes.elements) { false } : codes.eq(c)
498
556
  sel = sel.strip_mask(false) if sel.has_mask?
499
557
  idx = [nil] * h.ndim
@@ -503,9 +561,8 @@ class CACategoricalIterator < CASegmentIterator
503
561
  full_c = resolve_axis_codes(codes, h.shape, axis)
504
562
  exclude = none ? CArray.boolean(*h.shape) { true } : full_c.eq(c).not
505
563
  exclude = exclude | full_c.is_masked if full_c.has_mask?
506
- local = h.copy
507
- local[exclude] = UNDEF
508
- local
564
+ # a copy with the rest masked, which a read-only Face also gives
565
+ h.mask_where(exclude)
509
566
  end
510
567
  end
511
568
  private :axis_by_core, :axis_moments_by_core, :group_cells
@@ -524,13 +581,43 @@ class CACategoricalIterator < CASegmentIterator
524
581
  if kernel_scans?(op, answer)
525
582
  bundle = @k > 0 ? [@cat.codes, @k, [axis]]
526
583
  : [CArray.int64(h.shape[axis]).fill(-1), 1, [axis]]
527
- return h.__axis_group_scan__([axis], [bundle], op)
584
+ return h.__scan_by_axis_groups__([axis], [bundle], op)
528
585
  end
529
586
  each_group_along(axis, ->(shape) { blank_like(answer, shape) }) do |g|
530
587
  g.public_send(op, axis: axis)
531
588
  end
532
589
  end
533
590
 
591
+ # #lag along an axis (case A): each group's positions along the axis, every
592
+ # other axis kept, lagged along it, so a lag never reaches another group.
593
+ def axis_lag (n, axis)
594
+ axis = checked_axis(axis, :lag)
595
+ classified_along!(:lag, axis)
596
+ each_group_along(axis, ->(shape) { blank_like(@value, shape) }) do |g|
597
+ g.lag(n, axis: axis)
598
+ end
599
+ end
600
+
601
+ # #strip_mask(method:) along an axis (case A): each group's positions along
602
+ # the axis filled along it, fiber by fiber.
603
+ def axis_hold (method, axis)
604
+ axis = checked_axis(axis, :strip_mask)
605
+ classified_along!(:strip_mask, axis)
606
+ each_group_along(axis, ->(shape) { blank_like(@value, shape) }) do |g|
607
+ g.strip_mask(method: method, axis: axis)
608
+ end
609
+ end
610
+
611
+ # #order along an axis (case A): each group's positions along the axis
612
+ # ranked along it, fiber by fiber.
613
+ def axis_order (axis, **kw)
614
+ axis = checked_axis(axis, :order)
615
+ classified_along!(:order, axis)
616
+ each_group_along(axis, ->(shape) { CArray.int64(*shape) }) do |g|
617
+ g.order(axis: axis, **kw)
618
+ end
619
+ end
620
+
534
621
  # #map along an axis (case A): the block receives each group's positions
535
622
  # along the axis, every other axis kept, and its answer goes back to them.
536
623
  def axis_map (axis, data_type, &block)
@@ -566,7 +653,7 @@ class CACategoricalIterator < CASegmentIterator
566
653
  "positions along axis #{axis} (shape [#{n}]); got shape " \
567
654
  "#{@cat.codes.shape.inspect}"
568
655
  end
569
- private :axis_scan, :axis_map, :each_group_along, :classified_along!
656
+ private :axis_scan, :axis_lag, :axis_order, :axis_hold, :axis_map, :each_group_along, :classified_along!
570
657
 
571
658
  # Axis-aware wsum + wmean fused (single kernel call, both outputs). Returns
572
659
  # [wsum_ca, wmean_ca]. Weights must match source shape exactly (explicit
@@ -582,6 +669,7 @@ class CACategoricalIterator < CASegmentIterator
582
669
  "must equal source.shape #{h.shape.inspect}. Wrap 1-D / band-shape " \
583
670
  "weights via `.broadcast_to(*source.shape)` before passing."
584
671
  end
672
+ return weighted_by_core(weights, axis) unless kernel_reads?(h.data_type)
585
673
  codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
586
674
  weights_f64 = weights.data_type == CA_FLOAT64 ? weights : weights.float64
587
675
  band = h.shape.dup; band.delete_at(axis)
@@ -592,10 +680,25 @@ class CACategoricalIterator < CASegmentIterator
592
680
  [ws_out, wm_out]
593
681
  end
594
682
 
683
+ # wsum and wmean of each group by the core: the group's cells of the value
684
+ # and of the weights, each other cell masked, reduced along the axis.
685
+ def weighted_by_core (weights, axis)
686
+ h = @value
687
+ %i[wsum wmean].map do |op|
688
+ answers = (0...[@k, 1].max).map do |c|
689
+ group_cells(h, axis, c, @k.zero?)
690
+ .public_send(op, group_cells(weights, axis, c, @k.zero?), axis: axis)
691
+ end
692
+ out = CArray.stack(answers, axis: 0).copy
693
+ @k.zero? ? out[0...0, *([nil] * (out.ndim - 1))].copy : out
694
+ end
695
+ end
696
+ private :weighted_by_core
697
+
595
698
  # Axis-aware prod: dedicated kernel (identity 1.0, separate from moments to
596
699
  # avoid conflating with sum's zero-identity memset).
597
700
  def axis_prod (axis)
598
- return axis_by_core(axis, :prod) unless MONOID_TYPES.include?(@value.data_type)
701
+ return axis_by_core(axis, :prod) unless kernel_reads?(@value.data_type)
599
702
  h = @value
600
703
  axis = checked_axis(axis, :prod)
601
704
  codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
@@ -1,7 +1,7 @@
1
1
  # Complex-component accessors (real / real= / imag / imag= / real? /
2
- # is_real). Loaded eagerly BEFORE carray/lazy.rb, because lazy.rb does
3
- # `alias_method :__real_eager__, :real` (and the imag equivalent) at load
4
- # time -- these defs must exist by then.
2
+ # is_real). Loaded eagerly BEFORE carray/lazy.rb, because lazy.rb takes
3
+ # `instance_method(:real)` (and the imag equivalent) at load time -- these
4
+ # defs must exist by then.
5
5
 
6
6
  class CArray
7
7
 
@@ -40,9 +40,9 @@ class CArray
40
40
  # Architecturally:
41
41
  # - lib/carray/lazy.rb's `real`/`imag` overrides dispatch to CAMonOp
42
42
  # when self is a lazy view, and fall through to the defs here for eager
43
- # parents (via `alias_method :__real_eager__, :real` at load time).
43
+ # parents (via `instance_method(:real)` taken at load time).
44
44
  # This file MUST be required before carray/lazy.rb (see carray.rb), and
45
- # removing these defs would break that alias and the lazy.rb load.
45
+ # removing these defs would break the lazy.rb load.
46
46
  #
47
47
  # - ext/ca_obj_stride.c recognises `CAField .real/.imag over complex`
48
48
  # as a first-class byte-mismatched reinterpret pattern. Architectural
@@ -28,13 +28,18 @@ class CArray
28
28
  # The result's data type is the common type of the two branches; a
29
29
  # branch given as `UNDEF` masks its cells and leaves the type to the
30
30
  # other branch (`cond.then_else(a, UNDEF)` has `a`'s data type).
31
+ # String branches make the result a {CAString}
32
+ # (`cond.then_else("yes", "no")`, also with `UNDEF` for one of them).
33
+ # A Symbol branch, or a String beside a value of another kind, makes
34
+ # the result an object array.
31
35
  # When `self` or a branch is a lazy view, the result is a lazy view
32
36
  # (read-only) that joins the expression around it; `copy` makes an
33
37
  # array of it. An object result is always computed at once.
34
38
  # @param x [CArray, Numeric, Object] true-branch value(s).
35
39
  # @param y [CArray, Numeric, Object] false-branch value(s).
36
- # @return [CArray] new array with the same shape as `self`, or a
37
- # lazy view (CATriOp) when an operand is lazy.
40
+ # @return [CArray] new array with the same shape as `self` (a
41
+ # {CAString} for String branches), or a lazy view (CATriOp) when an
42
+ # operand is lazy.
38
43
  # @raise [ArgumentError] when `self` is not a boolean CArray, or when
39
44
  # an array branch's shape does not pair with `self`'s as an
40
45
  # operator's operands would (equal, or size-1 axes of the same ndim;
@@ -52,15 +57,10 @@ class CArray
52
57
  face = [x, y].find { |v| v.is_a?(CArray) && !v.scalar? && v.face? }
53
58
  if face
54
59
  refuse_branch_shape(x, y) if reshaped
60
+ refuse_branch_mix(x, y, face)
55
61
  return face_then_else(x, y, face)
56
62
  end
57
- # Promote data_type from both branches via CArray.result_type, which
58
- # lets a scalar branch take the type of an array branch as an operator
59
- # does (cond.then_else(0, int32_array) is int32). UNDEF is a missing
60
- # value, not a value of some type: a branch that is UNDEF leaves the
61
- # type to the other.
62
- typed = [x, y].reject { |v| v.equal?(UNDEF) }
63
- dt = typed.empty? ? CA_OBJECT : CArray.result_type(*typed)
63
+ dt = CArray.send(:selection_type, [x, y])
64
64
  return lazy_then_else(x, y, dt) if lazy_then_else?(x, y, dt)
65
65
  # A branch shaped unlike the condition goes through the lazy select,
66
66
  # which pairs the shapes by the operator rule (equal, or size-1 axes
@@ -84,7 +84,7 @@ class CArray
84
84
  if self.has_mask?
85
85
  result[self.is_masked] = UNDEF
86
86
  end
87
- result
87
+ CArray.send(:selection_text?, [x, y]) ? CAString.wrap(result) : result
88
88
  end
89
89
 
90
90
  # A lazy receiver or branch makes the selection a lazy node (CATriOp
@@ -93,7 +93,7 @@ class CArray
93
93
  # a CScalar of it, UNDEF a masked one.
94
94
  private def lazy_then_else? (x, y, dt)
95
95
  return false if scalar? || dt == CA_OBJECT || dt == CA_FIXLEN
96
- [self, x, y].any? { |v| v.is_a?(CArray) && v.__lazy_view__? }
96
+ [self, x, y].any? { |v| v.is_a?(CArray) && v.lazy_view? }
97
97
  end
98
98
 
99
99
  # The types the lazy select cannot carry keep the strict form: a full
@@ -104,6 +104,22 @@ class CArray
104
104
  "then_else: branch shape #{bad.shape.inspect} does not match the condition's #{shape.inspect}"
105
105
  end
106
106
 
107
+ # An array branch beside a Face branch must be a Face of the same kind
108
+ # (the same obj_type, so a CARecord subclass and a CARecord of one data
109
+ # class pair, as their store takes): the Face decides what a cell means,
110
+ # so a plain array would be read as its storage (an integer as a time's
111
+ # ticks) and the result would take the class of whichever branch came
112
+ # second. A scalar branch is a value and goes through the Face's store.
113
+ private def refuse_branch_mix (x, y, face)
114
+ arrays = [x, y].select { |v| v.is_a?(CArray) && !v.scalar? }
115
+ return if arrays.all? { |v| v.face? && v.obj_type == face.obj_type }
116
+ hint = face.is_a?(CAString) ?
117
+ " (CArray.string(a) makes a CAString of an object array of Strings)" : ""
118
+ raise ArgumentError,
119
+ "then_else: cannot mix Face and non-Face arrays, or arrays of " \
120
+ "different Face classes; convert them first to one Face class or to no Face" + hint
121
+ end
122
+
107
123
  private def lazy_then_else (x, y, dt)
108
124
  branch = lambda do |v|
109
125
  next v if v.is_a?(CArray) && !v.scalar?
@@ -178,7 +194,9 @@ class CArray
178
194
  # Scalar returns from a callable (e.g. `->(v) { 0 }`) broadcast
179
195
  # to the subset shape. The result `data_type` is the promotion
180
196
  # of the two subset results via `CArray.result_type`, or
181
- # `data_type` when given. Masked cells in `cond` propagate to
197
+ # `data_type` when given. A String result is a value (not the name
198
+ # of a data type); when both callables give Strings or {CAString}s
199
+ # the result is a {CAString}. Masked cells in `cond` propagate to
182
200
  # `UNDEF` in the result.
183
201
  #
184
202
  # @param cond [CArray] boolean selector; same shape as `self`.
@@ -205,18 +223,19 @@ class CArray
205
223
  x_else = self[cond.not]
206
224
  y_then = then_fn.call(x_then)
207
225
  y_else = else_fn.call(x_else)
226
+ text = !data_type && CArray.send(:selection_text?, [y_then, y_else])
208
227
 
209
228
  # A callable that returns a scalar (e.g. `->(v) { 0 }`) broadcasts
210
229
  # to the subset shape; wrap it here so the scatter step below sees a
211
230
  # same-length CArray.
212
231
  unless y_then.is_a?(CArray)
213
- y_then = CArray.new(data_type || CArray.result_type(y_then), x_then.shape).fill(y_then)
232
+ y_then = CArray.new(data_type || CArray.send(:selection_type, [y_then]), x_then.shape).fill(y_then)
214
233
  end
215
234
  unless y_else.is_a?(CArray)
216
- y_else = CArray.new(data_type || CArray.result_type(y_else), x_else.shape).fill(y_else)
235
+ y_else = CArray.new(data_type || CArray.send(:selection_type, [y_else]), x_else.shape).fill(y_else)
217
236
  end
218
237
 
219
- dt = data_type || CArray.result_type(y_then, y_else)
238
+ dt = data_type || CArray.send(:selection_type, [y_then, y_else])
220
239
  out = CArray.new(dt, self.shape)
221
240
  out[cond] = y_then
222
241
  out[cond.not] = y_else
@@ -225,7 +244,7 @@ class CArray
225
244
  # positions, so both scatters leave the cell untouched — an explicit
226
245
  # fix-up is required.
227
246
  out[cond.is_masked] = UNDEF if cond.has_mask?
228
- out
247
+ text ? CAString.wrap(out) : out
229
248
  end
230
249
 
231
250
  # @overload select(condlist, choicelist, default: 0, data_type: nil)
@@ -238,7 +257,9 @@ class CArray
238
257
  # Each `choicelist[k]` is either a same-shape CArray or a scalar
239
258
  # broadcast to every cell. The result `data_type` is the promotion
240
259
  # of every choice plus `default` via `CArray.result_type`, or
241
- # `data_type` when given.
260
+ # `data_type` when given. A String choice is a value (not the name of
261
+ # a data type); when every choice and `default` is a String or a
262
+ # {CAString}, the result is a {CAString}.
242
263
  #
243
264
  # @param condlist [Array<CArray>] boolean selectors.
244
265
  # @param choicelist [Array<CArray, Numeric, Object>] values, one
@@ -274,7 +295,7 @@ class CArray
274
295
  end
275
296
  shape = first.shape
276
297
 
277
- dt = data_type || CArray.result_type(*choicelist, default)
298
+ dt = data_type || selection_type(choicelist + [default])
278
299
  # `default` can be either a same-shape CArray (per-cell fallback) or a
279
300
  # scalar (broadcast to every cell).
280
301
  default_full = default.is_a?(CArray) && !default.scalar?
@@ -297,7 +318,35 @@ class CArray
297
318
  v = choicelist[k]
298
319
  out[c] = v.is_a?(CArray) ? v[c] : v
299
320
  end
321
+ if !data_type && selection_text?(choicelist + [default])
322
+ out = CAString.wrap(out)
323
+ end
300
324
  out
301
325
  end
302
326
 
327
+ # The data type a selection among `values` writes into: their common type,
328
+ # by the rule an operator promotes its operands by (a scalar takes the
329
+ # type of an array beside it). UNDEF is a missing value, not a value of
330
+ # some type, so it leaves the type to the others. A String or Symbol is a
331
+ # value, which CArray.result_type would read as the name of a data type
332
+ # ("int32"), so it makes the result object.
333
+ def self.selection_type (values)
334
+ typed = values.reject { |v| v.equal?(UNDEF) }
335
+ if typed.empty? || typed.any? { |v| v.is_a?(String) || v.is_a?(Symbol) }
336
+ CA_OBJECT
337
+ else
338
+ CArray.result_type(*typed)
339
+ end
340
+ end
341
+ private_class_method :selection_type
342
+
343
+ # Whether a selection among `values` gives text: every value that is not
344
+ # UNDEF is a String or a CAString, and there is at least one. The result
345
+ # is then a CAString rather than an object array of Strings.
346
+ def self.selection_text? (values)
347
+ present = values.reject { |v| v.equal?(UNDEF) }
348
+ !present.empty? && present.all? { |v| v.is_a?(String) || v.is_a?(CAString) }
349
+ end
350
+ private_class_method :selection_text?
351
+
303
352
  end
@@ -55,7 +55,7 @@ class CArray
55
55
  # @return [CAConstString]
56
56
  def self.const_string (arg, encoding: Encoding::UTF_8, &block)
57
57
  if arg.is_a?(CArray)
58
- return string_face_of(arg).to_const_string(encoding: encoding)
58
+ return string_face_of(arg, "CArray.const_string").to_const_string(encoding: encoding)
59
59
  end
60
60
  if block
61
61
  n = Integer(arg)
@@ -135,7 +135,7 @@ class CAConstString
135
135
  def sort_addr (*args, **kw)
136
136
  # The native scan has no notion of an incomparable sentinel, so a masked
137
137
  # column goes the same way a per-axis one does.
138
- return __sort_addr_bytes__ if args.empty? && kw.empty? && ! has_mask?
138
+ return sort_addr_by_bytes if args.empty? && kw.empty? && ! has_mask?
139
139
  to_string.sort_addr(*args, **kw)
140
140
  end
141
141
 
@@ -143,6 +143,12 @@ class CAConstString
143
143
  # Returns the per-fiber indices that index a sort by string order.
144
144
  # @return [CArray] `:int64` indices.
145
145
  def sort_index (*args, **kw)
146
+ if args.empty? && kw.empty? && ! has_mask?
147
+ # With no axis the indices are those of the flattened array, which are
148
+ # its addresses.
149
+ addr = sort_addr_by_bytes
150
+ return ndim == 1 ? addr : addr.flatten.copy
151
+ end
146
152
  to_string.sort_index(*args, **kw)
147
153
  end
148
154
 
@@ -163,7 +169,7 @@ class CAConstString
163
169
  # every cell is masked or the array is empty.
164
170
  # @return [String, CAConstString]
165
171
  def min (*args, **kw)
166
- return __min_bytes__ || UNDEF if args.empty? && kw.empty?
172
+ return min_string_by_bytes || UNDEF if args.empty? && kw.empty?
167
173
  const_string_lift(to_string.min(*args, **kw))
168
174
  end
169
175
 
@@ -171,7 +177,7 @@ class CAConstString
171
177
  # Byte-largest counterpart of {#min}.
172
178
  # @return [String, CAConstString]
173
179
  def max (*args, **kw)
174
- return __max_bytes__ || UNDEF if args.empty? && kw.empty?
180
+ return max_string_by_bytes || UNDEF if args.empty? && kw.empty?
175
181
  const_string_lift(to_string.max(*args, **kw))
176
182
  end
177
183
 
@@ -319,6 +325,9 @@ class CAConstString
319
325
  # @overload categorize(labels: nil, sort_labels: false)
320
326
  # @return [CACategorical] categories keyed on the strings.
321
327
  def categorize (labels: nil, sort_labels: false)
328
+ # Discovery reads the bytes (a String per distinct value only); a given
329
+ # vocabulary is matched through the strings.
330
+ return super if labels.nil?
322
331
  to_string.categorize(labels: labels, sort_labels: sort_labels)
323
332
  end
324
333
 
@@ -340,7 +349,7 @@ class CAConstString
340
349
  # @!method ge(other)
341
350
  # @return [CArray] boolean, true where self sorts after or equals `other`.
342
351
  [:ne, :lt, :le, :gt, :ge].each do |op|
343
- define_method(op) { |other| to_string.public_send(op, string_operand(other)) }
352
+ define_method(op, &Ractor.make_shareable(proc { |other| to_string.public_send(op, string_operand(other)) }))
344
353
  end
345
354
 
346
355
  alias < lt
@@ -287,7 +287,7 @@ class CArray
287
287
  "(its contents are left undefined); use CArray.new to fill."
288
288
  end
289
289
  if args.size == 2 and args[1].is_a?(Array)
290
- return __empty__(*args, **options)
290
+ return empty_of_data_type(*args, **options)
291
291
  end
292
292
  unless options.empty?
293
293
  raise ArgumentError,
@@ -314,32 +314,36 @@ class CArray
314
314
  CArray.new(self::DataType, shape)
315
315
  end
316
316
 
317
- # @overload from_memory_view(obj)
317
+ # @overload from_memory_view(obj, mask: nil)
318
318
  # Returns a fresh CArray interpreted as this typed subclass's
319
319
  # `data_type`, imported by copy from a MemoryView producer.
320
320
  # Forwards to
321
- # `CArray.from_memory_view(obj, data_type: self::DataType)`; the
322
- # result is always 1-D (chain `.reshape` for higher rank).
321
+ # `CArray.from_memory_view(obj, data_type: self::DataType)`. A
322
+ # typed producer keeps its shape and must agree with the type; a
323
+ # typeless one is read as 1-D (chain `.reshape` for higher rank).
323
324
  # @param obj [Object] MemoryView producer.
325
+ # @param mask [Object, nil] MemoryView producer of the mask.
324
326
  # @return [CArray]
325
- def from_memory_view (obj)
326
- CArray.from_memory_view(obj, data_type: self::DataType)
327
+ def from_memory_view (obj, mask: nil)
328
+ CArray.from_memory_view(obj, data_type: self::DataType, mask: mask)
327
329
  end
328
330
 
329
- # @overload wrap_memory_view(obj)
331
+ # @overload wrap_memory_view(obj, mask: nil)
330
332
  # Returns a zero-copy CAWrap over `obj`'s MemoryView interpreted
331
333
  # as this typed subclass's `data_type`. Forwards to
332
- # `CArray.wrap_memory_view(obj, data_type: self::DataType)`; the
333
- # result is always 1-D (chain `.reshape` for higher rank).
334
+ # `CArray.wrap_memory_view(obj, data_type: self::DataType)`. A
335
+ # typed producer keeps its shape and must agree with the type; a
336
+ # typeless one is read as 1-D (chain `.reshape` for higher rank).
334
337
  #
335
338
  # Here the receiver names the element type, not the class of the
336
339
  # result: the call forwards with `CArray` as the receiver, so a
337
340
  # `CAWrap` comes back. Choosing the class is the other reading of
338
341
  # the receiver, and it lives on `CArray.wrap_memory_view` itself.
339
342
  # @param obj [Object] MemoryView producer.
343
+ # @param mask [Object, nil] MemoryView producer of the mask.
340
344
  # @return [CAWrap]
341
- def wrap_memory_view (obj)
342
- CArray.wrap_memory_view(obj, data_type: self::DataType)
345
+ def wrap_memory_view (obj, mask: nil)
346
+ CArray.wrap_memory_view(obj, data_type: self::DataType, mask: mask)
343
347
  end
344
348
 
345
349
  end
@@ -164,6 +164,7 @@ class CArray
164
164
  # midpoints go the same way as numpy's integer linspace).
165
165
  float_out = CArray.new(CA_FLOAT64, [n]).span(x1.to_f..x2.to_f)
166
166
  return float_out if data_type == CA_FLOAT64
167
+ # coerce: the type the caller asked for
167
168
  float_out.floor.to_type(data_type)
168
169
  end
169
170