carray 3.0.4 → 3.0.6

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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +438 -0
  3. data/README.md +1 -1
  4. data/carray.gemspec +3 -2
  5. data/ext/ca_array_pool.c +25 -1
  6. data/ext/ca_axis_dispatch.c +19 -20
  7. data/ext/ca_axis_group.c +1 -1
  8. data/ext/ca_categorical_iterator.c +34 -27
  9. data/ext/ca_compare.c +4 -4
  10. data/ext/ca_composite_dispatch.c +9 -9
  11. data/ext/ca_kernel_iterator.c +122 -237
  12. data/ext/ca_kernel_iterator.h +522 -337
  13. data/ext/ca_obj_array.c +69 -19
  14. data/ext/ca_obj_bincmp.c +10 -1
  15. data/ext/ca_obj_binop.c +11 -1
  16. data/ext/ca_obj_bitarray.c +5 -1
  17. data/ext/ca_obj_bitfield.c +60 -43
  18. data/ext/ca_obj_byte_swap.c +4 -1
  19. data/ext/ca_obj_const_string.c +15 -3
  20. data/ext/ca_obj_face.c +44 -1
  21. data/ext/ca_obj_fake.c +3 -1
  22. data/ext/ca_obj_field.c +13 -3
  23. data/ext/ca_obj_grid.c +219 -17
  24. data/ext/ca_obj_meld.c +69 -113
  25. data/ext/ca_obj_moncmp.c +9 -1
  26. data/ext/ca_obj_monop.c +86 -12
  27. data/ext/ca_obj_object.c +118 -32
  28. data/ext/ca_obj_record.c +3 -0
  29. data/ext/ca_obj_reduce.c +19 -1
  30. data/ext/ca_obj_refer.c +80 -26
  31. data/ext/ca_obj_remap.c +69 -10
  32. data/ext/ca_obj_roll.c +27 -6
  33. data/ext/ca_obj_select.c +38 -6
  34. data/ext/ca_obj_select_axis.c +1 -0
  35. data/ext/ca_obj_shift.c +32 -7
  36. data/ext/ca_obj_stack.c +311 -111
  37. data/ext/ca_obj_stride.c +159 -64
  38. data/ext/ca_obj_string.c +27 -2
  39. data/ext/ca_obj_tile.c +28 -7
  40. data/ext/ca_obj_transpose.c +8 -4
  41. data/ext/ca_obj_triop.c +12 -1
  42. data/ext/ca_obj_window.c +80 -50
  43. data/ext/ca_op_ipower.c +12 -12
  44. data/ext/ca_sort_kernels.h +6 -3
  45. data/ext/caframe_csv_reader.c +1 -1
  46. data/ext/carray.h +35 -1
  47. data/ext/carray_access.c +43 -67
  48. data/ext/carray_address_basis.c +134 -0
  49. data/ext/carray_attribute.c +32 -0
  50. data/ext/carray_broadcast.c +3 -4
  51. data/ext/carray_build_flags.h +1 -1
  52. data/ext/carray_cast.c +251 -93
  53. data/ext/carray_cast_func.rb +56 -26
  54. data/ext/carray_conversion.c +11 -7
  55. data/ext/carray_core.c +814 -72
  56. data/ext/carray_count.c +6 -0
  57. data/ext/carray_element.c +22 -13
  58. data/ext/carray_factorize.c +20 -20
  59. data/ext/carray_generate.c +8 -2
  60. data/ext/carray_histogram.c +26 -4
  61. data/ext/carray_index_classifier.c +4 -4
  62. data/ext/carray_internal.h +159 -12
  63. data/ext/carray_kernels_bincmp.c +2174 -170
  64. data/ext/carray_kernels_binop.c +25653 -5487
  65. data/ext/carray_kernels_init.c +8 -8
  66. data/ext/carray_kernels_moncmp.c +96 -96
  67. data/ext/carray_kernels_monop.c +12253 -5235
  68. data/ext/carray_kernels_reduce_aggregate.c +6150 -2423
  69. data/ext/carray_kernels_reduce_boolean.c +1533 -15
  70. data/ext/carray_kernels_reduce_cumulative.c +2442 -882
  71. data/ext/carray_kernels_reduce_extreme.c +5658 -1661
  72. data/ext/carray_kernels_reduce_variance.c +2534 -402
  73. data/ext/carray_kernels_scan.c +368 -368
  74. data/ext/carray_kernels_search.c +4551 -604
  75. data/ext/carray_kernels_sort.c +659 -751
  76. data/ext/carray_kernels_triop.c +114 -114
  77. data/ext/carray_key_windows.c +4 -0
  78. data/ext/carray_lazy.c +47 -0
  79. data/ext/carray_loop.c +1 -1
  80. data/ext/carray_mask.c +175 -161
  81. data/ext/carray_median_percentile.c +4 -4
  82. data/ext/carray_memory_view.c +15 -9
  83. data/ext/carray_operator.c +152 -49
  84. data/ext/carray_order.c +230 -10
  85. data/ext/carray_random.c +22 -19
  86. data/ext/carray_scatter.c +24 -7
  87. data/ext/carray_slab.c +128 -29
  88. data/ext/carray_sort.c +224 -16
  89. data/ext/carray_sort_kernel.c +5 -7
  90. data/ext/carray_struct.c +35 -43
  91. data/ext/carray_test.c +74 -19
  92. data/ext/carray_time_text.c +6 -4
  93. data/ext/carray_utils.c +63 -21
  94. data/ext/extconf.rb +11 -0
  95. data/ext/mkkernel.rb +546 -199
  96. data/ext/ruby_carray.c +4 -1
  97. data/ext/version.h +4 -4
  98. data/lib/carray/attribute.rb +42 -5
  99. data/lib/carray/autoload_carray.rb +3 -3
  100. data/lib/carray/autoload_method_extension.rb +5 -3
  101. data/lib/carray/axis_group.rb +13 -7
  102. data/lib/carray/basics.rb +28 -8
  103. data/lib/carray/block_iterator.rb +17 -3
  104. data/lib/carray/categorical.rb +35 -23
  105. data/lib/carray/categorical_iterator.rb +8 -8
  106. data/lib/carray/complex.rb +2 -2
  107. data/lib/carray/conditional.rb +3 -2
  108. data/lib/carray/const_string.rb +10 -0
  109. data/lib/carray/data_type_extension.rb +8 -1
  110. data/lib/carray/fixlen_string.rb +41 -2
  111. data/lib/carray/frame/column_verbs.rb +48 -16
  112. data/lib/carray/frame/concat.rb +17 -3
  113. data/lib/carray/frame/convert.rb +19 -0
  114. data/lib/carray/frame/csv_parser.rb +32 -32
  115. data/lib/carray/frame/describe.rb +4 -2
  116. data/lib/carray/frame/frame.rb +405 -85
  117. data/lib/carray/frame/group.rb +777 -64
  118. data/lib/carray/frame/io.rb +212 -72
  119. data/lib/carray/frame/join.rb +146 -37
  120. data/lib/carray/frame/records.rb +3 -3
  121. data/lib/carray/frame/reshape.rb +89 -11
  122. data/lib/carray/frame/sort.rb +57 -9
  123. data/lib/carray/frame/verbs.rb +384 -49
  124. data/lib/carray/frame/windows.rb +44 -6
  125. data/lib/carray/fuse_source.rb +31 -2
  126. data/lib/carray/inspect.rb +7 -2
  127. data/lib/carray/iterator.rb +32 -0
  128. data/lib/carray/lazy.rb +52 -8
  129. data/lib/carray/mask_gap_fill.rb +37 -17
  130. data/lib/carray/meld_reduce.rb +1 -1
  131. data/lib/carray/methods/choose.rb +3 -2
  132. data/lib/carray/methods/is_in.rb +8 -1
  133. data/lib/carray/methods/meshgrid.rb +3 -1
  134. data/lib/carray/runtime.rb +47 -8
  135. data/lib/carray/segment_iterator.rb +35 -16
  136. data/lib/carray/serialize.rb +53 -19
  137. data/lib/carray/stack.rb +33 -18
  138. data/lib/carray/string.rb +8 -0
  139. data/lib/carray/string_operation_extension.rb +13 -5
  140. data/lib/carray/struct_builder.rb +5 -3
  141. data/lib/carray/time.rb +112 -68
  142. data/lib/carray/window_iterator.rb +16 -4
  143. data/yard-stubs/ca_obj_array.rb +26 -1
  144. data/yard-stubs/ca_obj_transpose.rb +5 -4
  145. data/yard-stubs/carray_attribute.rb +3 -1
  146. data/yard-stubs/carray_cast.rb +10 -5
  147. data/yard-stubs/carray_conversion.rb +13 -3
  148. data/yard-stubs/carray_element.rb +7 -6
  149. data/yard-stubs/carray_mask.rb +28 -4
  150. data/yard-stubs/carray_math.rb +45 -20
  151. data/yard-stubs/carray_memory_view.rb +7 -0
  152. data/yard-stubs/carray_order.rb +100 -4
  153. data/yard-stubs/carray_scatter.rb +7 -5
  154. data/yard-stubs/carray_test.rb +7 -4
  155. metadata +18 -4
data/ext/ruby_carray.c CHANGED
@@ -14,6 +14,7 @@
14
14
  ---------------------------------------------------------------------------- */
15
15
 
16
16
  #include "carray.h"
17
+ #include "carray_internal.h"
17
18
  #include "carray_build_flags.h"
18
19
  #include "version.h"
19
20
 
@@ -313,7 +314,7 @@ Init_carray_ext (void)
313
314
  Init_carray_sort(); /* sort / sort_copy / sort_addr / axis2addr */
314
315
  Init_carray_count();
315
316
  Init_carray_kernels(); /* mkkernel-generated: reductions + maps */
316
- Init_carray_round(); /* round(ndigits); after the generated round */
317
+ Init_carray_round(); /* round(ndigits) / round!(ndigits) */
317
318
 
318
319
  Init_carray_utils();
319
320
 
@@ -394,5 +395,7 @@ Init_carray_ext (void)
394
395
  Init_carray_slab();
395
396
 
396
397
  Init_carray_address_basis();
398
+
399
+ ca_obj_num_builtin = ca_obj_num;
397
400
  }
398
401
 
data/ext/version.h CHANGED
@@ -1,6 +1,6 @@
1
- #define CA_VERSION "3.0.4"
2
- #define CA_VERSION_CODE 30004
1
+ #define CA_VERSION "3.0.6"
2
+ #define CA_VERSION_CODE 30006
3
3
  #define CA_VERSION_MAJOR 3
4
4
  #define CA_VERSION_MINOR 0
5
- #define CA_VERSION_TEENY 4
6
- #define CA_VERSION_DATE "2026/10/19"
5
+ #define CA_VERSION_TEENY 6
6
+ #define CA_VERSION_DATE "2026/10/28"
@@ -6,6 +6,7 @@
6
6
  # ca.set_attr(:unit, "m/s") # => setter; writes to self's @attr
7
7
  # ca.set_attrs(other.attrs) # => sets every key of a Hash at once
8
8
  # ca.attrs # => frozen shallow clone of the full Hash
9
+ # ca.attrs_symkey # => the same, with Symbol keys (nested too)
9
10
  # ca.has_attr? # => any attribute present?
10
11
  # ca.has_attr?(:unit) # => specific key present?
11
12
  #
@@ -33,14 +34,17 @@
33
34
  # parent's cells, so the parent's attributes do not describe them.
34
35
  # `a.lazy` itself (CALazyMarker) is a view of `a` and walks through,
35
36
  # and so does a lazy type conversion (the cast lane `promote_list`
36
- # and `wrap_readonly` build), for the reason `to_type` carries them.
37
+ # and `wrap_readonly` build), for the reason `to_type` carries them,
38
+ # and `swap_bytes`: like `refer`, `bitarray` and `bitfield`, a view
39
+ # that reads the same cells another way carries them.
37
40
  # A view of a Face shows the attributes of the Face it was taken
38
41
  # from (the walk goes to that Face, which the view's own parent
39
42
  # chain skips).
40
43
  # A view of several arrays (CAStack / CAMeld, so `stack`, `meld` and
41
44
  # `montage`) shows, below its own @attr, the keys every one of its
42
45
  # arrays shows with the same value; a key they disagree on, or one
43
- # some of them lack, is not shown.
46
+ # some of them lack, is not shown. An array with no cells is left out
47
+ # of that count, unless all of them are empty.
44
48
  #
45
49
  # Stored values are frozen copies, nested Arrays and Hashes included, so
46
50
  # an attribute changes only through set_attr / set_attrs: a value read
@@ -58,7 +62,9 @@
58
62
  # `categorize`, a CAFrame `cast` / `parse_to_time` / `to_time`) carries
59
63
  # them. Arithmetic and reductions do not -- the result is a different
60
64
  # quantity -- including those that pick a value (`first`, `last`, `min`,
61
- # `max`, `unique`). A CAFrame index built from a key column's values
65
+ # `max`, `unique`) and the element-wise ones that keep the unit (`abs`,
66
+ # `round`, `clip`): an attribute does not say which operations leave it
67
+ # true, so none carries it. Set it on the result again. A CAFrame index built from a key column's values
62
68
  # (group and pivot) shows the key column's attributes.
63
69
 
64
70
  class CArray
@@ -157,6 +163,15 @@ class CArray
157
163
  (merged || {}).freeze
158
164
  end
159
165
 
166
+ # @overload attrs_symkey
167
+ # Returns {#attrs} with its keys as Symbols, frozen. The keys of
168
+ # Hashes nested in the values, inside Arrays too, become Symbols
169
+ # as well.
170
+ # @return [Hash{Symbol => Object}]
171
+ def attrs_symkey
172
+ return CArray.__send__(:attr_symbolize_keys, self.attrs)
173
+ end
174
+
160
175
  # @overload has_attr?
161
176
  # Returns whether `self` (or any parent view) has any attribute
162
177
  # set.
@@ -235,7 +250,12 @@ class CArray
235
250
  end
236
251
 
237
252
  # The attributes every array in `list` shows with the same value.
253
+ # An array with no cells holds no values for the attributes to describe,
254
+ # so it has no say (unless every one is empty), as CAFrame leaves a frame
255
+ # with no rows out of a meld.
238
256
  def self.attrs_shared_by (list)
257
+ with_cells = list.reject { |a| a.elements == 0 }
258
+ list = with_cells unless with_cells.empty?
239
259
  first, *rest = list.map(&:attrs)
240
260
  return {} if first.nil?
241
261
  first.select { |k, v| rest.all? { |h| h.key?(k) && h[k] == v } }
@@ -243,7 +263,10 @@ class CArray
243
263
  private_class_method :attrs_shared_by
244
264
 
245
265
  def attr_computing_view? (ca)
246
- return ca.__op_id__ < CAMonOp::CAST_BASE if ca.is_a?(CAMonOp)
266
+ if ca.is_a?(CAMonOp)
267
+ op = ca.__op_id__
268
+ return op < CAMonOp::CAST_BASE && op != CAMonOp::OP_BYTE_SWAP
269
+ end
247
270
  ca.is_a?(CABinOp) || ca.is_a?(CATriOp) ||
248
271
  ca.is_a?(CAMonCmp) || ca.is_a?(CABinCmp)
249
272
  end
@@ -304,6 +327,20 @@ class CArray
304
327
  v
305
328
  end
306
329
  end
307
- private_class_method :attr_normalize_key, :attr_validate_value, :attr_coerce_value
330
+ # A stored value with the keys of every Hash in it as Symbols, frozen
331
+ # as the stored value is.
332
+ def self.attr_symbolize_keys (v)
333
+ case v
334
+ when Array then v.map { |e| attr_symbolize_keys(e) }.freeze
335
+ when Hash
336
+ v.each_with_object({}) do |(k, vv), h|
337
+ h[k.to_sym] = attr_symbolize_keys(vv)
338
+ end.freeze
339
+ else
340
+ v
341
+ end
342
+ end
343
+ private_class_method :attr_normalize_key, :attr_validate_value, :attr_coerce_value,
344
+ :attr_symbolize_keys
308
345
 
309
346
  end
@@ -32,12 +32,12 @@ class CArray
32
32
  autoload_method "marshal_load", serialize_rb
33
33
  end
34
34
 
35
- # A view, a wrap and a scalar are marshalled through _dump / self._load,
35
+ # A view, a mask, a wrap and a scalar are marshalled through _dump / self._load,
36
36
  # which return a new array: with marshal_dump Marshal records the class and
37
37
  # loads into an empty instance of it, which the plain array the payload
38
38
  # holds cannot become. A record array keeps marshal_dump, under which it
39
39
  # already round-trips with its class.
40
- [CAView, CAWrap, CScalar].each do |klass|
40
+ [CAView, CAWrap, CScalar, CArrayMask, CAObjectMask].each do |klass|
41
41
  klass.class_eval do
42
42
  undef_method :marshal_dump
43
43
  autoload_method "_dump", "carray/serialize"
@@ -137,7 +137,7 @@ autoload :CAUnion, "carray/struct"
137
137
  # ---- CAFrame (DataFrame over CArray columns) -------------------------------
138
138
  #
139
139
  # The frame is only reached through the CAFrame constant (CAFrame.new /
140
- # .from_csv / subclassing) or GroupedFrame (obtained from CAFrame#group_by,
140
+ # .read_csv / subclassing) or GroupedFrame (obtained from CAFrame#group_by,
141
141
  # by which point CAFrame is already loaded). Both constants live in the same
142
142
  # require bundle (carray/frame.rb), so autoloading either loads the whole
143
143
  # frame. Nothing in the eager core references them, so it loads on first use.
@@ -31,9 +31,11 @@ module AutoloadMethodExtension
31
31
 
32
32
  private
33
33
 
34
- def autoload_define (target, name, library, original_spec)
35
- library = Ractor.make_shareable(library.to_s.dup)
36
- original_spec = Ractor.make_shareable(original_spec.to_s.dup)
34
+ def autoload_define (target, name, library_given, spec_given)
35
+ # The stub reads these two, so they are never assigned again: Ruby 4.0
36
+ # refuses to make a Proc shareable when a variable it reads may be.
37
+ library = Ractor.make_shareable(library_given.to_s.dup)
38
+ original_spec = Ractor.make_shareable(spec_given.to_s.dup)
37
39
  # Shareable, so the stub answers in any Ractor. Only the main Ractor
38
40
  # loads the library: Ractors that require at the same time can deadlock
39
41
  # (Ruby 3.4), so another Ractor is told what to load there instead.
@@ -409,7 +409,7 @@ class CAGroupIterator
409
409
  # except for a band folded into the count, which has no route but the
410
410
  # engine.
411
411
  def core_counts? (kw)
412
- return false if CArray.__fast_paths_enabled__
412
+ return false if CArray.__fast_paths_enabled__(:axis_group_count_kernel)
413
413
  has_group, fused = AxisGroup.parse_axis(kw[:axis])
414
414
  has_group && fused.empty?
415
415
  end
@@ -494,8 +494,8 @@ class CAGroupIterator
494
494
  # Weighted mean = Sum(v*w) / Sum(w), both over the combined present-set. The
495
495
  # `value*0 + weights` denominator carries the same value|weight mask, so its
496
496
  # group-sum is Sum(w) over exactly the cells value*weight used. An empty group
497
- # (no present pair) is UNDEF (matching CArray#wmean); a present group whose
498
- # weights sum to zero yields NaN/Inf (core's 0/0).
497
+ # (no present pair) and a group whose weights sum to zero are UNDEF, as
498
+ # CArray#wmean leaves both.
499
499
  def wmean (weights, **kw)
500
500
  has_group, = AxisGroup.parse_axis(kw[:axis])
501
501
  return value.wmean(weights, **kw) unless has_group
@@ -508,7 +508,7 @@ class CAGroupIterator
508
508
  # No present pair is UNDEF (an empty group), set on the denominator so
509
509
  # the division never meets an empty group's 0 / 0.
510
510
  den = den.copy
511
- den[cnt.eq(0)] = UNDEF
511
+ den[cnt.eq(0).or(den.eq(0))] = UNDEF
512
512
  # min_count: counts the present pairs, as the core wmean does.
513
513
  pairs = regroup(prod, spec)
514
514
  pairs.finish_reduction(:wmean, kw, num / den)
@@ -863,7 +863,7 @@ class CAGroupIterator
863
863
  # pass. With the fast paths off it goes through the band loop instead,
864
864
  # which then has one position.
865
865
  def whole_block? (bslots)
866
- bslots.empty? && CArray.__fast_paths_enabled__
866
+ bslots.empty? && CArray.__fast_paths_enabled__(:axis_group_whole_block)
867
867
  end
868
868
 
869
869
  # Iterate the band positions of a band-preserving grouping. For each band
@@ -923,9 +923,15 @@ class CAGroupIterator
923
923
  # paths off (a development build run with CARRAY_GENERAL_PATH set) each
924
924
  # group is reduced by the core instead -- except a band folded into the
925
925
  # statistic (+fused+), which has no route but the kernel.
926
+ #
927
+ # With no band axis the core groups the whole value in one pass, which is
928
+ # faster than the kernel for every group size (1M cells on an M2 Max: 7.5
929
+ # to 12.5 ms against 15 ms), so the kernel is taken only with a band.
926
930
  def kernel_reads_value? (fused = [])
927
931
  return false if value.face? || ! (value.integer? || value.float?)
928
- CArray.__fast_paths_enabled__ || ! fused.empty?
932
+ return true unless fused.empty?
933
+ return false if spec.slot_meta.none? { |m| m[:kind] == :band }
934
+ CArray.__fast_paths_enabled__(:axis_group_reduce_kernel)
929
935
  end
930
936
 
931
937
  # Whether the scan kernel gives the core's answer: it reads an integer,
@@ -933,7 +939,7 @@ class CAGroupIterator
933
939
  # float64 (an object in object), cummax / cummin in the payload's data type
934
940
  # and cumcount in int64 -- not the core's answer for a boolean's running sum.
935
941
  def kernel_scans? (op)
936
- return false unless CArray.__fast_paths_enabled__
942
+ return false unless CArray.__fast_paths_enabled__(:axis_group_scan_kernel)
937
943
  return false if value.face? || value.complex?
938
944
  return true if value.object? || op == :cumcount
939
945
  core = CAIterator.core_probe(value).public_send(op).data_type
data/lib/carray/basics.rb CHANGED
@@ -117,13 +117,15 @@ class CArray
117
117
  end
118
118
 
119
119
  # @overload drop_axis
120
- # Returns a view of `self` with every size-1 axis dropped.
120
+ # Returns a view of `self` with every size-1 axis dropped. When every
121
+ # axis has size 1, one axis of size 1 is kept.
121
122
  # @return [CArray] view with reduced `ndim`.
122
123
  def drop_axis
123
124
  if ndim == 1
124
- return self[]
125
+ return scalar? ? self : self[]
125
126
  else
126
127
  newdim = shape.reject{|x| x == 1 }
128
+ newdim = [1] if newdim.empty? # every axis has size 1: keep one
127
129
  return ( ndim != newdim.size ) ? reshape(*newdim) : self[]
128
130
  end
129
131
  end
@@ -189,7 +191,13 @@ class CArray
189
191
  # @param src [CArray] source array.
190
192
  # @return [self]
191
193
  # @raise [ArgumentError] when `offset.length != self.ndim`.
194
+ # @raise [FrozenError] when `self` is frozen.
192
195
  def paste (offset, src)
196
+ # The write goes through a window view, which is not frozen itself:
197
+ # refuse a frozen self here, as a write into it directly is refused.
198
+ if frozen?
199
+ raise FrozenError.new("can't modify frozen #{self.class}: #{inspect}", receiver: self)
200
+ end
193
201
  raise ArgumentError, "offset length must equal ndim" if offset.length != ndim
194
202
  # A zero-length source covers no cell, so there is nothing to write --
195
203
  # and the empty range it would ask for is not a window CAWindow can
@@ -310,8 +318,7 @@ class CArray
310
318
  # @overload clip(min, max = nil, fill_value = nil, lfill: nil, ufill: nil)
311
319
  # Returns `self` with every element clamped to `[min, max]`.
312
320
  #
313
- # Either bound may be `nil` for a one-sided clip; that side
314
- # dispatches to the `pmax` / `pmin` binop kernels. When
321
+ # Either bound may be `nil` for a one-sided clip. When
315
322
  # `fill_value` (or `lfill` / `ufill`) is given, out-of-range
316
323
  # cells are replaced by the fill instead of clamped -- pass
317
324
  # `UNDEF` to mask that end. `fill_value` is sugar for symmetric
@@ -320,6 +327,8 @@ class CArray
320
327
  #
321
328
  # `min` may not exceed `max` (in any cell, when a bound is an
322
329
  # array); equal bounds give every unmasked cell that value.
330
+ # A NaN cell of `self` stays NaN, one-sided or two-sided (unlike
331
+ # `pmax` / `pmin`, which answer the other operand for it).
323
332
  # A NaN in a bound array leaves that cell unbounded on that side,
324
333
  # as `nil` does for the whole side. A bound is never UNDEF or
325
334
  # masked: a missing bound is not read as either, so fill it first.
@@ -373,19 +382,30 @@ class CArray
373
382
 
374
383
  if lfill.nil? && ufill.nil?
375
384
  return clamp_between(min, max) if !min.nil? && !max.nil?
376
- return pmax(min) if max.nil?
377
- return pmin(max)
385
+ return clamp_below(min) if max.nil?
386
+ return clamp_above(max)
378
387
  end
379
388
 
380
389
  # A side without a fill still clamps.
381
390
  out = self.copy
382
- out = out.pmax(min) if !min.nil? && lfill.nil?
383
- out = out.pmin(max) if !max.nil? && ufill.nil?
391
+ out = out.clamp_below(min) if !min.nil? && lfill.nil?
392
+ out = out.clamp_above(max) if !max.nil? && ufill.nil?
384
393
  out[self.lt(min)] = lfill unless min.nil? || lfill.nil?
385
394
  out[self.gt(max)] = ufill unless max.nil? || ufill.nil?
386
395
  out
387
396
  end
388
397
 
398
+ # A one-sided clip of a float array passes a NaN cell through, as the
399
+ # two-sided one does: the open side is a NaN bound, which leaves it
400
+ # unbounded. pmax / pmin would answer the bound for a NaN cell.
401
+ protected def clamp_below (min)
402
+ float? ? clamp_between(min, Float::NAN) : pmax(min)
403
+ end
404
+
405
+ protected def clamp_above (max)
406
+ float? ? clamp_between(Float::NAN, max) : pmin(max)
407
+ end
408
+
389
409
  # `contains` was retired in favour of {#is_in} (value-hash membership).
390
410
  # `a.contains(v1, v2)` -> `a.is_in([v1, v2])`;
391
411
  # `a.contains(v1, v2, axis: k)` -> `a.is_in([v1, v2]).any(axis: k)`.
@@ -74,6 +74,14 @@ class CABlockIterator < CAIterator
74
74
  "blocks: expected #{source.ndim} tile sizes (one per axis), " \
75
75
  "got #{blocks.size}"
76
76
  end
77
+ # Each tile is read through a view with one axis per tile position and
78
+ # one per cell in the tile, twice the source's rank.
79
+ if 2 * source.ndim > CA_RANK_MAX
80
+ raise ArgumentError,
81
+ "blocks: a rank #{source.ndim} array would need #{2 * source.ndim} " \
82
+ "axes for its tiles, beyond the limit of #{CA_RANK_MAX}; " \
83
+ "blocks works up to rank #{CA_RANK_MAX / 2}"
84
+ end
77
85
 
78
86
  @sndim = source.ndim
79
87
  offsets = Array.new(@sndim, 0)
@@ -93,7 +101,9 @@ class CABlockIterator < CAIterator
93
101
  # Absorb a leading offset with a zero-copy pre-slice, so the tile geometry
94
102
  # below always starts at index 0.
95
103
  @source =
96
- if offsets.all?(&:zero?)
104
+ if source.scalar?
105
+ source.flatten # a CScalar takes no index; its one-cell view does
106
+ elsif offsets.all?(&:zero?)
97
107
  source
98
108
  else
99
109
  source[*offsets.map { |o| o..-1 }]
@@ -517,7 +527,7 @@ class CABlockIterator < CAIterator
517
527
  sview = saddr[*strip_ranges].block_view(*tiles)
518
528
  grid = (0...@sndim).map { |i| vview.shape[i] }
519
529
  cells = tiles.inject(1) { |p, t| p * t }
520
- order = vview.copy.reshape(*(grid + [cells])).sort_addr(axis: @sndim)
530
+ order = vview.copy.reshape(*(grid + [cells])).sort_index(axis: @sndim)
521
531
  src_sorted = sview.reshape(*(grid + [cells])).take_along_axis(order, axis: @sndim)
522
532
  out[*strip_ranges].block_view(*tiles)[] = src_sorted.reshape(*(grid + tiles))
523
533
  end
@@ -817,7 +827,7 @@ class CABlockIterator < CAIterator
817
827
  # A padded entity whose interior holds the source and whose partial-edge
818
828
  # margin is masked (UNDEF), sized to exactly cover the ceil tile grid. When
819
829
  # the source already divides evenly this is the source itself (no copy), so
820
- # the tiles alias it. Memoised; feeds the iterate escape hatches.
830
+ # the tiles alias it. Kept for one call; feeds the iterate escape hatches.
821
831
  def padded_source
822
832
  @padded_source ||= begin
823
833
  pshape = @sndim.times.map { |i| @shape[i] * @sizes[i] }
@@ -845,6 +855,8 @@ class CABlockIterator < CAIterator
845
855
  def lag (*args, **keywords) = @source.__send__(:carry_attrs, super)
846
856
  def strip_mask (**keywords) = @source.__send__(:carry_attrs, super)
847
857
  end)
858
+
859
+ read_source_per_call :@padded_source, :@tile_grid_view
848
860
  end
849
861
 
850
862
 
@@ -857,6 +869,8 @@ class CArray
857
869
  # first for "valid" tiling.
858
870
  # @param blocks [Array<Integer, Range>] per-axis tile sizes (or ranges).
859
871
  # @return [CABlockIterator]
872
+ # @raise [ArgumentError] when `self` has a rank above `CA_RANK_MAX / 2`
873
+ # (8): the tiles are read through a view of twice the rank.
860
874
  def blocks (*blocks)
861
875
  CABlockIterator.new(self, *blocks)
862
876
  end
@@ -299,7 +299,7 @@ class CACategorical < CAObject
299
299
  # `codes[i] = x` raises; use `codes.copy` for a mutable copy.
300
300
  # @return [CArray]
301
301
  def codes
302
- parent
302
+ face_storage
303
303
  end
304
304
 
305
305
  # Face hook: carry the vocabulary across lifted views (slice / reshape / …).
@@ -328,10 +328,17 @@ class CACategorical < CAObject
328
328
  # Face hook: decode a per-cell code into its category label, through the one
329
329
  # decode every code-to-label path shares (see #label_at).
330
330
  def storage_to_element(raw)
331
- code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw
331
+ code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(face_storage.data_type)) : raw
332
332
  label_at(code)
333
333
  end
334
334
 
335
+ # Face hook: a value written into a cell (a fill_value of shift and
336
+ # window) is a label, and the cell holds its code.
337
+ # @raise [ArgumentError] if the value is not one of the labels.
338
+ def element_to_storage (value)
339
+ code_for_fill(value)
340
+ end
341
+
335
342
  # ---- category-space operations (by label, not code) -------------------
336
343
  #
337
344
  # The element value of a categorical IS its category, so these specialise
@@ -389,7 +396,14 @@ class CACategorical < CAObject
389
396
  # (0 for an unknown label).
390
397
  # @param label [Object] category to count.
391
398
  # @return [Integer]
392
- def count(label)
399
+ # @overload count
400
+ # Returns the number of cells in some category (not masked), as
401
+ # {CArray#count} with no value does.
402
+ # @return [Integer]
403
+ def count(*args)
404
+ return count_not_masked if args.empty?
405
+ raise ArgumentError, "count: wrong number of arguments (given #{args.size}, expected 0..1)" if args.size > 1
406
+ label = args[0]
393
407
  code = @labels.index(label)
394
408
  code ? codes.count(code) : 0
395
409
  end
@@ -400,9 +414,9 @@ class CACategorical < CAObject
400
414
  #
401
415
  # Memoised: the codes are read-only (immutable storage), so the counts are a
402
416
  # pure function of the categorical and stay valid for its lifetime. The same
403
- # cached array backs {#reduceat_index} and {#sort_addr}, so a wide aggregate
404
- # over the same categorical pays the count once. (Do not mutate the returned
405
- # array — it is shared; take `.copy` for a scratch buffer.) A derived-view /
417
+ # cached array backs {#reduceat_index} and {#group_addr}, so a wide aggregate
418
+ # over the same categorical pays the count once. The returned array is shared,
419
+ # so it is frozen; take `.copy` for a scratch buffer. A derived-view /
406
420
  # composite categorical caches too: read-only rides from the codes as a flag,
407
421
  # not a Ruby freeze, so the object stays non-frozen and the memo ivar sticks.
408
422
  # @overload category_sizes
@@ -415,8 +429,7 @@ class CACategorical < CAObject
415
429
  bc = codes.bincount
416
430
  out = CArray.new(bc.data_type, [@labels.size], bytes: bc.bytes) # new zero-fills
417
431
  out[0...bc.elements] = bc if bc.elements > 0
418
- @category_sizes = out
419
- out
432
+ @category_sizes = out.freeze
420
433
  end
421
434
 
422
435
  # Back-compatible alias; {#category_sizes} is the canonical name (a histogram
@@ -543,7 +556,7 @@ class CACategorical < CAObject
543
556
  # The code a fill value stands for: UNDEF stays UNDEF, a label is its code.
544
557
  def code_for_fill (value)
545
558
  return UNDEF if UNDEF.equal?(value)
546
- code = labels.index(value)
559
+ code = label_position(value)
547
560
  raise ArgumentError, "#{value.inspect} is not one of the labels #{labels.inspect}" unless code
548
561
  code
549
562
  end
@@ -583,27 +596,27 @@ class CACategorical < CAObject
583
596
  # two accessors are the building blocks for that other engine — lay the data
584
597
  # out as category-contiguous blocks, then select per block.
585
598
  #
586
- # The grouping plan — sort_addr (the counting sort, the dominant cost) plus
599
+ # The grouping plan — group_addr (the counting sort, the dominant cost) plus
587
600
  # reduceat_index and category_sizes — depends only on the codes, which are
588
601
  # read-only. It is therefore memoised (a pure function of the categorical): the
589
602
  # first grouping access builds it and every later one reuses it. This is what
590
603
  # lets a wide aggregate (many payload columns over one classifier) or a
591
604
  # CACategoricalIterator run the counting sort once, not once per column /
592
605
  # iterator. Only the plan is cached; the payload-dependent grouped copy
593
- # (value.reshape(n)[sort_addr]) is rebuilt per column (it varies with value).
606
+ # (value.reshape(n)[group_addr]) is rebuilt per column (it varies with value).
594
607
 
595
- # Force-build (and cache) the whole grouping plan up front — sort_addr,
608
+ # Force-build (and cache) the whole grouping plan up front — group_addr,
596
609
  # reduceat_index, category_sizes — for prepare-ahead use before a batch of
597
610
  # groupings. Lazy building already covers correctness; this is the explicit
598
611
  # "pay the counting sort now" handle (e.g. right after `df.group_by(col)`).
599
612
  # Returns self so it chains.
600
613
  # @overload build_grouping
601
- # Eagerly builds and caches the grouping plan (sort_addr / reduceat_index
614
+ # Eagerly builds and caches the grouping plan (group_addr / reduceat_index
602
615
  # / category_sizes). Optional — the plan is built lazily on first grouping
603
616
  # access — but useful to pay the counting sort once ahead of a batch.
604
617
  # @return [self]
605
618
  def build_grouping
606
- sort_addr # pulls category_sizes; reduceat_index shares category_sizes
619
+ group_addr # pulls category_sizes; reduceat_index shares category_sizes
607
620
  reduceat_index
608
621
  self
609
622
  end
@@ -617,26 +630,25 @@ class CACategorical < CAObject
617
630
  # order. The first {#category_sizes}.sum addresses are exactly the classified
618
631
  # cells in category order — CACategoricalIterator's permutation. For an N-D
619
632
  # categorical the addresses are into the raveled storage, so
620
- # `value.reshape(elements)[cat.sort_addr]` produces the contiguous blocks.
633
+ # `value.reshape(elements)[cat.group_addr]` produces the contiguous blocks.
621
634
  # Memoised (see the grouping-plan note above): the counting sort is the
622
635
  # dominant grouping cost, so it is computed once and shared by every payload
623
636
  # column and by CACategoricalIterator, and kept for the categorical's
624
- # lifetime (an int64 per cell). Do not mutate the returned array.
625
- # @overload sort_addr
637
+ # lifetime (an int64 per cell). The returned array is shared, so it is frozen.
638
+ # @overload group_addr
626
639
  # Returns a length-{#elements} integer CArray of flat storage
627
640
  # addresses that order the cells by category (excluded cells last).
628
641
  # @return [CArray]
629
- def sort_addr
630
- return @sort_addr if @sort_addr
642
+ def group_addr
643
+ return @group_addr if @group_addr
631
644
  out = codes.reshape(elements).__send__(:addresses_by_category,
632
645
  reduceat_index.copy, @labels.size,
633
646
  category_sizes.sum)
634
- @sort_addr = out
635
- out
647
+ @group_addr = out.freeze
636
648
  end
637
649
 
638
650
  # Segment start offsets into the category-contiguous layout produced by
639
- # {#sort_addr}: reduceat_index[c] is where category c's block begins, so
651
+ # {#group_addr}: reduceat_index[c] is where category c's block begins, so
640
652
  # block c spans `reduceat_index[c] ... reduceat_index[c] + category_sizes[c]`.
641
653
  # Empty categories repeat the following start (a zero-width block). Length k,
642
654
  # aligned to #labels; pair with {#category_sizes} for the block lengths. On
@@ -649,7 +661,7 @@ class CACategorical < CAObject
649
661
  return @reduceat_index if @reduceat_index
650
662
  # Every boundary but the last; the copy makes it an entity the
651
663
  # reduceat kernels can read directly.
652
- @reduceat_index = CArray.segment_offsets(lengths: category_sizes)[0...-1].copy
664
+ @reduceat_index = CArray.segment_offsets(lengths: category_sizes)[0...-1].copy.freeze
653
665
  end
654
666
 
655
667
  # @overload categorize(labels: nil, sort_labels: false)
@@ -15,7 +15,7 @@
15
15
  # grp.median # order statistics share the same surface
16
16
  # grp.stddev
17
17
  #
18
- # This is the consumer of the sort_addr / reduceat_index foundation on
18
+ # This is the consumer of the group_addr / reduceat_index foundation on
19
19
  # CACategorical. Order statistics (median / percentile / quantile) cannot be
20
20
  # scattered — they need every value of a group held together — so the values
21
21
  # are materialized into contiguous blocks once, and every reduction (monoid or
@@ -82,7 +82,7 @@ class CACategoricalIterator < CASegmentIterator
82
82
  # once and shared by every payload column and iterator — see
83
83
  # CACategorical's grouping-plan note). The plan gives the segment STARTS
84
84
  # (reduceat_index) and the group-major permutation (perm[slot] = source index
85
- # at that grouped slot, = the valid prefix of sort_addr); gathering value
85
+ # at that grouped slot, = the valid prefix of group_addr); gathering value
86
86
  # through perm is the only per-column work. Excluded cells (masked or
87
87
  # out-of-vocabulary code) are absent from perm, so they never join a group;
88
88
  # the value mask rides the gather into the grouped copy.
@@ -109,7 +109,7 @@ class CACategoricalIterator < CASegmentIterator
109
109
  @offsets = cat.reduceat_index # cached segment STARTS (int64[k])
110
110
  @bounds = CArray.segment_offsets(lengths: @elements) # every boundary (int64[k+1])
111
111
  # The group-major source indices (#perm) are taken from the cached
112
- # sort_addr when first asked for.
112
+ # group_addr when first asked for.
113
113
  @nvalid = nvalid
114
114
  @codes = cat.codes.reshape(cat.elements) # flat codes view (map re-walk / weights)
115
115
  # Gather value into category-contiguous order via the cached permutation
@@ -295,7 +295,7 @@ class CACategoricalIterator < CASegmentIterator
295
295
  # @overload wmean(weights, axis:)
296
296
  # Per-fiber per-category weighted mean along `axis`. Same weights-shape
297
297
  # contract as {#wsum} (weights.shape == source.shape). Empty cell → MASKED;
298
- # a present cell whose weights sum to zero → NaN (0/0 core contract).
298
+ # a present cell whose weights sum to zero → UNDEF, as CArray#wmean.
299
299
  # @param weights [CArray]
300
300
  # @param axis [Integer]
301
301
  # @return [CArray]
@@ -316,12 +316,12 @@ class CACategoricalIterator < CASegmentIterator
316
316
 
317
317
  # Both are built with the grouping plan, and only on the flat path; the
318
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.
319
+ # Group-major source indices = the valid prefix of the cached group_addr.
320
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.
321
+ # group_addr would be out of range), so take the empty permutation directly.
322
322
  def perm
323
323
  if @nvalid && ! @perm
324
- @perm = @nvalid > 0 ? @cat.sort_addr[0...@nvalid] : CArray.int64(0)
324
+ @perm = @nvalid > 0 ? @cat.group_addr[0...@nvalid] : CArray.int64(0)
325
325
  end
326
326
  @perm
327
327
  end
@@ -551,7 +551,7 @@ class CACategoricalIterator < CASegmentIterator
551
551
  codes = @cat.codes
552
552
  # Classified along the axis, a group is taken by index; the general way,
553
553
  # masking the rest of a copy, gives the same cells.
554
- if codes.shape == [h.shape[axis]] && CArray.__fast_paths_enabled__
554
+ if codes.shape == [h.shape[axis]] && CArray.__fast_paths_enabled__(:categorical_group_by_index)
555
555
  sel = none ? CArray.boolean(codes.elements) { false } : codes.eq(c)
556
556
  sel = sel.strip_mask(false) if sel.has_mask?
557
557
  idx = [nil] * h.ndim
@@ -74,7 +74,7 @@ class CArray
74
74
  when CA_CMPLX128 then field(0, CA_FLOAT64)
75
75
  end
76
76
  else
77
- self[]
77
+ scalar? ? self : self[]
78
78
  end
79
79
  frozen? ? view : (@__real__ = view)
80
80
  end
@@ -100,7 +100,7 @@ class CArray
100
100
  end
101
101
  if not complex?
102
102
  zero = template { 0 }
103
- zero[is_masked] = UNDEF if has_mask?
103
+ zero.inherit_mask(self) if has_mask?
104
104
  return zero
105
105
  end
106
106
  return @__imag__ if part_of_self?(@__imag__)
@@ -219,8 +219,9 @@ class CArray
219
219
  "conditional: cond must be a boolean CArray with same shape as self"
220
220
  end
221
221
 
222
- x_then = self[cond]
223
- x_else = self[cond.not]
222
+ src = scalar? ? flatten : self # a CScalar takes no index
223
+ x_then = src[cond]
224
+ x_else = src[cond.not]
224
225
  y_then = then_fn.call(x_then)
225
226
  y_else = else_fn.call(x_else)
226
227
  text = !data_type && CArray.send(:selection_text?, [y_then, y_else])
@@ -92,6 +92,16 @@ class CAConstString
92
92
  # override the L1 generics where present.
93
93
  include CArray::StringOperationMixin
94
94
 
95
+ # Face hook: a value written into a cell (a fill_value of shift and
96
+ # window). A cell is a byte range of the shared buffer, which has no room
97
+ # for a String it does not hold, so only UNDEF can be written.
98
+ # @raise [ArgumentError] for any value but UNDEF.
99
+ def element_to_storage (value)
100
+ raise ArgumentError,
101
+ "a CAConstString can't hold a new String (its strings share one " \
102
+ "buffer); use UNDEF, or to_string first"
103
+ end
104
+
95
105
  # ---- ordering family ---------------------------------------------------
96
106
  #
97
107
  # A storage cell is a `(start, end)` byte range into the shared buffer, so