carray 3.0.3 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +469 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_array_pool.c +25 -1
  6. data/ext/ca_axis_descriptor.h +27 -24
  7. data/ext/ca_axis_dispatch.c +14 -26
  8. data/ext/ca_axis_group.c +27 -27
  9. data/ext/ca_binop_dispatch.c +1 -29
  10. data/ext/ca_binop_dispatch.h +0 -1
  11. data/ext/ca_categorical_iterator.c +231 -153
  12. data/ext/ca_compare.c +9 -20
  13. data/ext/ca_composite_dispatch.c +9 -9
  14. data/ext/ca_group_iter.c +16 -17
  15. data/ext/ca_kernel_iterator.c +1240 -2000
  16. data/ext/ca_kernel_iterator.h +502 -519
  17. data/ext/ca_monop_dispatch.c +82 -59
  18. data/ext/ca_monop_dispatch.h +1 -0
  19. data/ext/ca_obj_array.c +152 -45
  20. data/ext/ca_obj_bincmp.c +25 -21
  21. data/ext/ca_obj_binop.c +16 -26
  22. data/ext/ca_obj_bitarray.c +8 -3
  23. data/ext/ca_obj_bitfield.c +63 -49
  24. data/ext/ca_obj_block.c +18 -9
  25. data/ext/ca_obj_byte_swap.c +7 -7
  26. data/ext/ca_obj_const_string.c +188 -22
  27. data/ext/ca_obj_face.c +280 -58
  28. data/ext/ca_obj_face.h +19 -0
  29. data/ext/ca_obj_fake.c +15 -29
  30. data/ext/ca_obj_farray.c +6 -3
  31. data/ext/ca_obj_field.c +30 -8
  32. data/ext/ca_obj_fixlen_string.c +2 -3
  33. data/ext/ca_obj_grid.c +455 -134
  34. data/ext/ca_obj_meld.c +172 -34
  35. data/ext/ca_obj_moncmp.c +23 -8
  36. data/ext/ca_obj_monop.c +113 -67
  37. data/ext/ca_obj_object.c +210 -52
  38. data/ext/ca_obj_record.c +15 -7
  39. data/ext/ca_obj_reduce.c +17 -8
  40. data/ext/ca_obj_refer.c +42 -35
  41. data/ext/ca_obj_remap.c +30 -9
  42. data/ext/ca_obj_repeat.c +9 -10
  43. data/ext/ca_obj_roll.c +31 -10
  44. data/ext/ca_obj_select.c +48 -14
  45. data/ext/ca_obj_select_axis.c +15 -47
  46. data/ext/ca_obj_shift.c +41 -7
  47. data/ext/ca_obj_stack.c +180 -34
  48. data/ext/ca_obj_stride.c +190 -134
  49. data/ext/ca_obj_string.c +80 -4
  50. data/ext/ca_obj_tile.c +32 -11
  51. data/ext/ca_obj_time.c +22 -6
  52. data/ext/ca_obj_timedelta.c +22 -6
  53. data/ext/ca_obj_transpose.c +13 -7
  54. data/ext/ca_obj_triop.c +26 -32
  55. data/ext/ca_obj_window.c +267 -74
  56. data/ext/ca_op_ipower.c +4 -4
  57. data/ext/ca_sort_kernels.h +6 -3
  58. data/ext/ca_sweep_engine.c +2 -2
  59. data/ext/caframe_csv_reader.c +80 -1
  60. data/ext/carray.h +214 -75
  61. data/ext/carray_access.c +200 -99
  62. data/ext/carray_address_basis.c +343 -87
  63. data/ext/carray_attribute.c +113 -6
  64. data/ext/carray_bincount.c +10 -8
  65. data/ext/carray_broadcast.c +13 -8
  66. data/ext/carray_build_flags.h +3 -0
  67. data/ext/carray_call_cfunc.c +108 -0
  68. data/ext/carray_cast.c +375 -96
  69. data/ext/carray_cast_func.rb +90 -20
  70. data/ext/carray_class.c +4 -3
  71. data/ext/carray_config.h +28 -0
  72. data/ext/carray_conversion.c +18 -9
  73. data/ext/carray_copy.c +51 -14
  74. data/ext/carray_core.c +150 -55
  75. data/ext/carray_count.c +56 -20
  76. data/ext/carray_element.c +81 -33
  77. data/ext/carray_factorize.c +138 -84
  78. data/ext/carray_generate.c +199 -52
  79. data/ext/carray_histogram.c +63 -18
  80. data/ext/carray_hold.c +12 -15
  81. data/ext/carray_index_classifier.c +388 -176
  82. data/ext/carray_internal.h +348 -29
  83. data/ext/carray_kernels_bincmp.c +4453 -0
  84. data/ext/carray_kernels_binop.c +11368 -0
  85. data/ext/carray_kernels_init.c +1734 -0
  86. data/ext/carray_kernels_moncmp.c +2104 -0
  87. data/ext/carray_kernels_monop.c +18369 -0
  88. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  89. data/ext/carray_kernels_reduce_boolean.c +406 -0
  90. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  91. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  92. data/ext/carray_kernels_reduce_variance.c +3849 -0
  93. data/ext/carray_kernels_scan.c +4302 -0
  94. data/ext/carray_kernels_search.c +33829 -0
  95. data/ext/carray_kernels_sort.c +10897 -0
  96. data/ext/carray_kernels_triop.c +1864 -0
  97. data/ext/carray_key_windows.c +220 -0
  98. data/ext/carray_lazy.c +170 -111
  99. data/ext/carray_mask.c +337 -94
  100. data/ext/carray_median_percentile.c +82 -28
  101. data/ext/carray_memory_view.c +669 -561
  102. data/ext/carray_operator.c +129 -52
  103. data/ext/carray_order.c +604 -77
  104. data/ext/carray_partition.c +28 -27
  105. data/ext/carray_random.c +85 -31
  106. data/ext/carray_round.c +273 -0
  107. data/ext/carray_scatter.c +87 -45
  108. data/ext/carray_segment.c +3 -3
  109. data/ext/carray_slab.c +123 -37
  110. data/ext/carray_sort.c +259 -51
  111. data/ext/carray_sort_kernel.c +5 -7
  112. data/ext/carray_struct.c +54 -61
  113. data/ext/carray_test.c +26 -5
  114. data/ext/carray_time_text.c +4 -2
  115. data/ext/carray_undef.c +4 -0
  116. data/ext/carray_utils.c +309 -66
  117. data/ext/mk_call_cfunc.rb +19 -2
  118. data/ext/mkkernel.rb +403 -350
  119. data/ext/ruby_carray.c +10 -4
  120. data/ext/version.h +4 -4
  121. data/lib/carray/attribute.rb +139 -22
  122. data/lib/carray/autoload_carray.rb +8 -31
  123. data/lib/carray/autoload_method_extension.rb +14 -5
  124. data/lib/carray/axis_group.rb +255 -106
  125. data/lib/carray/basics.rb +105 -17
  126. data/lib/carray/bincount_nd.rb +12 -2
  127. data/lib/carray/block_iterator.rb +126 -23
  128. data/lib/carray/boolean_reduce.rb +17 -17
  129. data/lib/carray/categorical.rb +195 -59
  130. data/lib/carray/categorical_iterator.rb +132 -29
  131. data/lib/carray/complex.rb +5 -5
  132. data/lib/carray/conditional.rb +67 -18
  133. data/lib/carray/const_string.rb +24 -5
  134. data/lib/carray/construct.rb +15 -11
  135. data/lib/carray/data_type_extension.rb +9 -1
  136. data/lib/carray/distance_limit.rb +77 -0
  137. data/lib/carray/fixlen_string.rb +48 -3
  138. data/lib/carray/frame/column_verbs.rb +71 -0
  139. data/lib/carray/frame/concat.rb +17 -3
  140. data/lib/carray/frame/convert.rb +69 -0
  141. data/lib/carray/frame/csv_parser.rb +111 -17
  142. data/lib/carray/frame/describe.rb +3 -2
  143. data/lib/carray/frame/frame.rb +252 -28
  144. data/lib/carray/frame/group.rb +603 -101
  145. data/lib/carray/frame/io.rb +117 -28
  146. data/lib/carray/frame/join.rb +312 -51
  147. data/lib/carray/frame/records.rb +83 -7
  148. data/lib/carray/frame/reshape.rb +42 -35
  149. data/lib/carray/frame/sort.rb +7 -19
  150. data/lib/carray/frame/verbs.rb +188 -48
  151. data/lib/carray/frame/windows.rb +117 -0
  152. data/lib/carray/frame.rb +2 -0
  153. data/lib/carray/fuse_source.rb +42 -6
  154. data/lib/carray/fusion.rb +40 -16
  155. data/lib/carray/histogram.rb +6 -1
  156. data/lib/carray/inspect.rb +31 -8
  157. data/lib/carray/iterator.rb +85 -7
  158. data/lib/carray/key_windows.rb +620 -0
  159. data/lib/carray/lazy.rb +217 -183
  160. data/lib/carray/mask_gap_fill.rb +63 -57
  161. data/lib/carray/meld_reduce.rb +12 -14
  162. data/lib/carray/methods/bin.rb +26 -18
  163. data/lib/carray/methods/bincount.rb +16 -0
  164. data/lib/carray/methods/bit_string.rb +1 -1
  165. data/lib/carray/methods/choose.rb +16 -3
  166. data/lib/carray/methods/composition.rb +3 -1
  167. data/lib/carray/methods/gather_nd.rb +14 -5
  168. data/lib/carray/methods/insert_block.rb +2 -2
  169. data/lib/carray/methods/is_between.rb +59 -0
  170. data/lib/carray/methods/is_in.rb +61 -37
  171. data/lib/carray/methods/join.rb +1 -6
  172. data/lib/carray/methods/lag.rb +49 -0
  173. data/lib/carray/methods/locate_addr.rb +47 -51
  174. data/lib/carray/methods/mode.rb +1 -1
  175. data/lib/carray/methods/nunique.rb +11 -6
  176. data/lib/carray/methods/resize.rb +3 -3
  177. data/lib/carray/methods/value_counts.rb +4 -11
  178. data/lib/carray/record_order.rb +3 -3
  179. data/lib/carray/rng.rb +3 -0
  180. data/lib/carray/runtime.rb +53 -8
  181. data/lib/carray/segment_iterator.rb +323 -88
  182. data/lib/carray/serialize.rb +45 -14
  183. data/lib/carray/slab_iterator.rb +50 -0
  184. data/lib/carray/stack.rb +15 -12
  185. data/lib/carray/string.rb +113 -5
  186. data/lib/carray/string_operation_extension.rb +21 -6
  187. data/lib/carray/struct.rb +9 -21
  188. data/lib/carray/struct_builder.rb +24 -11
  189. data/lib/carray/time.rb +819 -158
  190. data/lib/carray/window_iterator.rb +122 -16
  191. data/lib/carray.rb +7 -0
  192. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  193. data/yard-stubs/ca_obj_field.rb +4 -0
  194. data/yard-stubs/ca_obj_grid.rb +21 -10
  195. data/yard-stubs/ca_obj_refer.rb +2 -1
  196. data/yard-stubs/ca_obj_roll.rb +2 -2
  197. data/yard-stubs/ca_obj_shift.rb +5 -1
  198. data/yard-stubs/ca_obj_tile.rb +1 -1
  199. data/yard-stubs/ca_obj_transpose.rb +5 -4
  200. data/yard-stubs/ca_obj_window.rb +8 -3
  201. data/yard-stubs/carray_access.rb +6 -1
  202. data/yard-stubs/carray_attribute.rb +3 -6
  203. data/yard-stubs/carray_cast.rb +12 -0
  204. data/yard-stubs/carray_conversion.rb +7 -2
  205. data/yard-stubs/carray_copy.rb +4 -2
  206. data/yard-stubs/carray_count.rb +4 -4
  207. data/yard-stubs/carray_element.rb +17 -10
  208. data/yard-stubs/carray_generate.rb +21 -1
  209. data/yard-stubs/carray_mask.rb +32 -3
  210. data/yard-stubs/carray_math.rb +41 -5
  211. data/yard-stubs/carray_median_percentile.rb +5 -2
  212. data/yard-stubs/carray_memory_view.rb +40 -23
  213. data/yard-stubs/carray_order.rb +129 -22
  214. data/yard-stubs/carray_random.rb +13 -4
  215. data/yard-stubs/carray_scatter.rb +17 -10
  216. data/yard-stubs/carray_slab.rb +9 -2
  217. data/yard-stubs/carray_sort.rb +6 -5
  218. data/yard-stubs/carray_test.rb +5 -3
  219. metadata +25 -3
  220. data/lib/carray/arrow_tensor.rb +0 -401
  221. data/lib/carray/table.rb +0 -108
data/lib/carray/basics.rb CHANGED
@@ -13,9 +13,19 @@
13
13
 
14
14
  class CArray
15
15
 
16
+ # The order sort puts two object values in (ca_obj_order_cmp in C): a
17
+ # Float NaN after every other value, NaN ties NaN, other pairs by `<=>`.
18
+ # nil when `<=>` cannot order the pair; the caller says what refused.
19
+ private_class_method def self.object_order_cmp (a, b)
20
+ a_nan = a.is_a?(Float) && a.nan?
21
+ b_nan = b.is_a?(Float) && b.nan?
22
+ return (a_nan ? 1 : 0) - (b_nan ? 1 : 0) if a_nan || b_nan
23
+ a <=> b
24
+ end
25
+
16
26
  # reshape / flatten / transpose! are defined in C
17
27
  # (ext/ca_obj_refer.c and ext/ca_obj_transpose.c). The bare size-1
18
- # insertion primitive is `__insert_axis_size1__` (C); `insert_axis` below
28
+ # insertion primitive is `insert_size_one_axes` (C); `insert_axis` below
19
29
  # is the user-facing method that owns the name and adds `repeat:`.
20
30
 
21
31
  # @overload insert_axis(*positions, repeat: nil)
@@ -58,7 +68,7 @@ class CArray
58
68
 
59
69
  # No repeat: -> plain size-1 insertion. The source-frame C primitive
60
70
  # handles normalization, range check and multiplicity directly.
61
- return __insert_axis_size1__(*flat) if repeat.nil?
71
+ return insert_size_one_axes(*flat) if repeat.nil?
62
72
 
63
73
  # Source frame: each position names the source axis the new axis goes
64
74
  # before. Gaps live in [0, ndim] (ndim = append at end); negatives count
@@ -98,7 +108,7 @@ class CArray
98
108
  final = {}
99
109
  order.each_with_index { |i, k| final[i] = gaps[i] + k }
100
110
 
101
- inter = __insert_axis_size1__(*order.map { |i| gaps[i] })
111
+ inter = insert_size_one_axes(*order.map { |i| gaps[i] })
102
112
  return inter unless order.any? { |i| reps[i] > 1 }
103
113
 
104
114
  shp = inter.shape
@@ -107,13 +117,15 @@ class CArray
107
117
  end
108
118
 
109
119
  # @overload drop_axis
110
- # 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.
111
122
  # @return [CArray] view with reduced `ndim`.
112
123
  def drop_axis
113
124
  if ndim == 1
114
125
  return self[]
115
126
  else
116
127
  newdim = shape.reject{|x| x == 1 }
128
+ newdim = [1] if newdim.empty? # every axis has size 1: keep one
117
129
  return ( ndim != newdim.size ) ? reshape(*newdim) : self[]
118
130
  end
119
131
  end
@@ -146,7 +158,7 @@ class CArray
146
158
  # out of range (a time array reads its bounds as times, a record array
147
159
  # orders by its declared members, a Face that cannot be ordered refuses),
148
160
  # and its store writes the bound or the fill value into them.
149
- private def __face_clip__ (min, max, fill_value, lfill, ufill)
161
+ private def face_clip (min, max, fill_value, lfill, ufill)
150
162
  lfill = fill_value if lfill.nil?
151
163
  ufill = fill_value if ufill.nil?
152
164
  out = copy
@@ -158,8 +170,8 @@ class CArray
158
170
  # An array of +shape+ of the same kind as self: its data type and cell
159
171
  # width, or its Face over a zero storage. Nothing is written into it, so
160
172
  # it can be built for a read-only Face too.
161
- private def __blank_like__ (shape)
162
- storage = __face_storage__
173
+ private def blank_like (shape)
174
+ storage = face_storage
163
175
  out = CArray.new(storage.data_type, shape, bytes: storage.bytes)
164
176
  face? ? out.face_lift(self) : out
165
177
  end
@@ -179,7 +191,13 @@ class CArray
179
191
  # @param src [CArray] source array.
180
192
  # @return [self]
181
193
  # @raise [ArgumentError] when `offset.length != self.ndim`.
194
+ # @raise [FrozenError] when `self` is frozen.
182
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
183
201
  raise ArgumentError, "offset length must equal ndim" if offset.length != ndim
184
202
  # A zero-length source covers no cell, so there is nothing to write --
185
203
  # and the empty range it would ask for is not a window CAWindow can
@@ -300,8 +318,7 @@ class CArray
300
318
  # @overload clip(min, max = nil, fill_value = nil, lfill: nil, ufill: nil)
301
319
  # Returns `self` with every element clamped to `[min, max]`.
302
320
  #
303
- # Either bound may be `nil` for a one-sided clip; that side
304
- # dispatches to the `pmax` / `pmin` binop kernels. When
321
+ # Either bound may be `nil` for a one-sided clip. When
305
322
  # `fill_value` (or `lfill` / `ufill`) is given, out-of-range
306
323
  # cells are replaced by the fill instead of clamped -- pass
307
324
  # `UNDEF` to mask that end. `fill_value` is sugar for symmetric
@@ -310,7 +327,11 @@ class CArray
310
327
  #
311
328
  # `min` may not exceed `max` (in any cell, when a bound is an
312
329
  # array); equal bounds give every unmasked cell that value.
313
- # A NaN bound bounds nothing.
330
+ # A NaN cell of `self` stays NaN, one-sided or two-sided (unlike
331
+ # `pmax` / `pmin`, which answer the other operand for it).
332
+ # A NaN in a bound array leaves that cell unbounded on that side,
333
+ # as `nil` does for the whole side. A bound is never UNDEF or
334
+ # masked: a missing bound is not read as either, so fill it first.
314
335
  #
315
336
  # Boundary is strict `[min, max]`: values equal to a bound
316
337
  # remain unchanged in both the clamped and filled variants.
@@ -325,7 +346,8 @@ class CArray
325
346
  # @param ufill [Object, nil] override above-range fill.
326
347
  # @return [CArray] new CArray with clamped or filled values.
327
348
  # @raise [ArgumentError] when both `min` and `max` are `nil`,
328
- # or when `min` exceeds `max`.
349
+ # when either is `UNDEF` or has a masked cell, or when `min`
350
+ # exceeds `max`.
329
351
  # @example
330
352
  # a.clip(0, 10) # strict clamp
331
353
  # a.clip(0, 10, -1) # both ends -> -1
@@ -334,7 +356,16 @@ class CArray
334
356
  if min.nil? && max.nil?
335
357
  raise ArgumentError, "clip: at least one of (min, max) must be given"
336
358
  end
337
- return __face_clip__(min, max, fill_value, lfill, ufill) if face?
359
+ [min, max].each do |bound|
360
+ undefined = UNDEF.equal?(bound) ||
361
+ (bound.is_a?(CArray) && bound.any_masked?) ||
362
+ (bound.is_a?(Array) && bound.flatten.any? { |v| UNDEF.equal?(v) })
363
+ next unless undefined
364
+ raise ArgumentError, "clip: a bound can not be UNDEF or masked " \
365
+ "(fill the bound first; a NaN bound leaves its cell " \
366
+ "unbounded, and UNDEF as the fill masks the cells out of range)"
367
+ end
368
+ return face_clip(min, max, fill_value, lfill, ufill) if face?
338
369
  if !min.nil? && !max.nil?
339
370
  reversed = if min.is_a?(CArray) then min.gt(max).any
340
371
  elsif max.is_a?(CArray) then max.lt(min).any
@@ -350,20 +381,31 @@ class CArray
350
381
  ufill = fill_value if ufill.nil?
351
382
 
352
383
  if lfill.nil? && ufill.nil?
353
- return __clip_ki__(min, max) if !min.nil? && !max.nil?
354
- return pmax(min) if max.nil?
355
- return pmin(max)
384
+ return clamp_between(min, max) if !min.nil? && !max.nil?
385
+ return clamp_below(min) if max.nil?
386
+ return clamp_above(max)
356
387
  end
357
388
 
358
389
  # A side without a fill still clamps.
359
390
  out = self.copy
360
- out = out.pmax(min) if !min.nil? && lfill.nil?
361
- 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?
362
393
  out[self.lt(min)] = lfill unless min.nil? || lfill.nil?
363
394
  out[self.gt(max)] = ufill unless max.nil? || ufill.nil?
364
395
  out
365
396
  end
366
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
+
367
409
  # `contains` was retired in favour of {#is_in} (value-hash membership).
368
410
  # `a.contains(v1, v2)` -> `a.is_in([v1, v2])`;
369
411
  # `a.contains(v1, v2, axis: k)` -> `a.is_in([v1, v2]).any(axis: k)`.
@@ -549,3 +591,49 @@ class Enumerator::ArithmeticSequence
549
591
  return CA_OBJECT(to_a)
550
592
  end
551
593
  end
594
+
595
+ class CArray
596
+ # map_slab / reduce_slab over a Face answer in that Face, as its other
597
+ # reductions do: each block value is written into a blank of the Face
598
+ # (which reads a CATime::Element, a string, a Face array ...) and handed
599
+ # to the slab engine as the storage it became; the result is lifted back
600
+ # at the end. A read-only Face has no blank to write into, so its values
601
+ # are collected as the surface objects they are. A `data_type:` from the
602
+ # caller is the caller's, and passes through.
603
+ module FaceSlabResult
604
+ def map_slab (**kw, &block)
605
+ return super unless face? && kw[:data_type].nil? && block
606
+ return super(**kw, data_type: :object, &block) if read_only?
607
+ out = super(**kw, data_type: face_storage.data_type) { |slab|
608
+ face_slab_storage(block.call(slab), slab.shape)
609
+ }
610
+ out.face_lift(self)
611
+ end
612
+
613
+ def reduce_slab (**kw, &block)
614
+ return super unless face? && kw[:data_type].nil? && block
615
+ return super(**kw, data_type: :object, &block) if read_only?
616
+ fiber = kw.key?(:init)
617
+ init = kw.delete(:init)
618
+ out = super(**kw, data_type: face_storage.data_type) { |slab|
619
+ value = if fiber
620
+ slab.to_a.flatten.inject(init) { |acc, e| block.call(acc, e) }
621
+ else
622
+ block.call(slab)
623
+ end
624
+ face_slab_storage(value, [1])[0]
625
+ }
626
+ out.face_lift(self)
627
+ end
628
+
629
+ private
630
+
631
+ def face_slab_storage (value, shape)
632
+ cell = blank_like(shape)
633
+ cell[] = value
634
+ cell.__send__(:face_storage)
635
+ end
636
+ end
637
+
638
+ prepend FaceSlabResult
639
+ end
@@ -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)