carray 3.0.2 → 3.0.3

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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +3 -4
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +225 -0
  6. data/ext/ca_axis_group.c +177 -99
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +66 -71
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +663 -214
  14. data/ext/ca_kernel_iterator.h +231 -167
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +132 -22
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +1 -0
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +96 -25
  25. data/ext/ca_obj_face.c +406 -64
  26. data/ext/ca_obj_face.h +92 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +21 -21
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +189 -107
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +543 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +47 -49
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -118
  46. data/ext/ca_obj_stride.c +195 -34
  47. data/ext/ca_obj_string.c +17 -18
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +149 -27
  54. data/ext/ca_op_ipower.c +24 -11
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_sort_kernels.h +70 -0
  58. data/ext/ca_sweep_engine.c +102 -79
  59. data/ext/ca_sweep_engine.h +7 -20
  60. data/ext/ca_triop_dispatch.c +4 -1
  61. data/ext/ca_triop_dispatch.h +5 -2
  62. data/ext/caframe_csv_reader.c +463 -0
  63. data/ext/carray.h +70 -27
  64. data/ext/carray_access.c +455 -211
  65. data/ext/carray_address_basis.c +177 -36
  66. data/ext/carray_attribute.c +45 -3
  67. data/ext/carray_bincount.c +32 -2
  68. data/ext/carray_broadcast.c +46 -14
  69. data/ext/carray_call_cfunc.c +0 -28
  70. data/ext/carray_cast.c +1444 -209
  71. data/ext/carray_cast_func.rb +34 -21
  72. data/ext/carray_conversion.c +197 -73
  73. data/ext/carray_copy.c +32 -11
  74. data/ext/carray_core.c +514 -122
  75. data/ext/carray_count.c +140 -87
  76. data/ext/carray_element.c +18 -27
  77. data/ext/carray_factorize.c +569 -277
  78. data/ext/carray_generate.c +76 -40
  79. data/ext/carray_histogram.c +152 -94
  80. data/ext/carray_hold.c +2 -1
  81. data/ext/carray_index_classifier.c +63 -96
  82. data/ext/carray_internal.h +385 -3
  83. data/ext/carray_lazy.c +834 -79
  84. data/ext/carray_loop.c +81 -65
  85. data/ext/carray_mask.c +682 -316
  86. data/ext/carray_math_kernel.h +1 -0
  87. data/ext/carray_median_percentile.c +221 -76
  88. data/ext/carray_memory_view.c +254 -248
  89. data/ext/carray_operator.c +1089 -359
  90. data/ext/carray_order.c +133 -69
  91. data/ext/carray_partition.c +102 -56
  92. data/ext/carray_random.c +273 -239
  93. data/ext/carray_scatter.c +308 -65
  94. data/ext/carray_segment.c +257 -0
  95. data/ext/carray_slab.c +170 -70
  96. data/ext/carray_sort.c +215 -218
  97. data/ext/carray_sort_kernel.c +8 -1
  98. data/ext/carray_struct.c +99 -21
  99. data/ext/carray_test.c +167 -64
  100. data/ext/carray_time_text.c +1182 -0
  101. data/ext/carray_utils.c +363 -98
  102. data/ext/extconf.rb +27 -2
  103. data/ext/mk_call_cfunc.rb +0 -2
  104. data/ext/mk_pow5_table.rb +35 -0
  105. data/ext/mkkernel.rb +1076 -846
  106. data/ext/ruby_carray.c +7 -1
  107. data/ext/version.h +4 -4
  108. data/ext/version.rb +4 -2
  109. data/lib/carray/attribute.rb +39 -0
  110. data/lib/carray/autoload_carray.rb +19 -0
  111. data/lib/carray/autoload_method_extension.rb +6 -0
  112. data/lib/carray/axis_group.rb +174 -26
  113. data/lib/carray/basics.rb +91 -25
  114. data/lib/carray/bincount_nd.rb +6 -0
  115. data/lib/carray/block_iterator.rb +118 -48
  116. data/lib/carray/boolean_reduce.rb +21 -29
  117. data/lib/carray/categorical.rb +94 -26
  118. data/lib/carray/categorical_iterator.rb +333 -826
  119. data/lib/carray/complex.rb +36 -29
  120. data/lib/carray/conditional.rb +94 -7
  121. data/lib/carray/const_string.rb +46 -0
  122. data/lib/carray/frame/concat.rb +157 -6
  123. data/lib/carray/frame/convert.rb +13 -7
  124. data/lib/carray/frame/csv_parser.rb +589 -82
  125. data/lib/carray/frame/describe.rb +79 -0
  126. data/lib/carray/frame/frame.rb +124 -25
  127. data/lib/carray/frame/group.rb +189 -63
  128. data/lib/carray/frame/io.rb +412 -72
  129. data/lib/carray/frame/join.rb +134 -46
  130. data/lib/carray/frame/records.rb +37 -16
  131. data/lib/carray/frame/reshape.rb +448 -0
  132. data/lib/carray/frame/sort.rb +26 -2
  133. data/lib/carray/frame/verbs.rb +715 -48
  134. data/lib/carray/frame.rb +2 -0
  135. data/lib/carray/fuse_source.rb +111 -2
  136. data/lib/carray/fusion.rb +229 -38
  137. data/lib/carray/inspect.rb +29 -11
  138. data/lib/carray/iterator.rb +14 -2
  139. data/lib/carray/lazy.rb +54 -30
  140. data/lib/carray/mask_gap_fill.rb +66 -52
  141. data/lib/carray/math.rb +32 -19
  142. data/lib/carray/meld_reduce.rb +40 -7
  143. data/lib/carray/methods/align_addr.rb +8 -4
  144. data/lib/carray/methods/bincount.rb +2 -2
  145. data/lib/carray/methods/bit_string.rb +14 -3
  146. data/lib/carray/methods/composition.rb +16 -10
  147. data/lib/carray/methods/factorize.rb +2 -2
  148. data/lib/carray/methods/gather_nd.rb +13 -2
  149. data/lib/carray/methods/insert_block.rb +9 -14
  150. data/lib/carray/methods/is_in.rb +117 -4
  151. data/lib/carray/methods/join.rb +1 -1
  152. data/lib/carray/methods/locate_addr.rb +177 -36
  153. data/lib/carray/methods/mode.rb +18 -1
  154. data/lib/carray/methods/repeat.rb +1 -20
  155. data/lib/carray/methods/resize.rb +21 -23
  156. data/lib/carray/methods/snap.rb +34 -12
  157. data/lib/carray/methods/unique.rb +11 -2
  158. data/lib/carray/record_order.rb +208 -0
  159. data/lib/carray/runtime.rb +14 -11
  160. data/lib/carray/segment_iterator.rb +990 -0
  161. data/lib/carray/serialize.rb +197 -27
  162. data/lib/carray/slab_iterator.rb +51 -27
  163. data/lib/carray/stack.rb +10 -4
  164. data/lib/carray/string_operation_extension.rb +4 -3
  165. data/lib/carray/struct.rb +142 -51
  166. data/lib/carray/struct_builder.rb +110 -14
  167. data/lib/carray/table.rb +2 -2
  168. data/lib/carray/time.rb +517 -103
  169. data/lib/carray/window_iterator.rb +127 -72
  170. data/lib/carray.rb +2 -2
  171. data/yard-stubs/ca_obj_array.rb +4 -4
  172. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  173. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  174. data/yard-stubs/ca_obj_fake.rb +1 -1
  175. data/yard-stubs/ca_obj_field.rb +1 -1
  176. data/yard-stubs/ca_obj_refer.rb +3 -3
  177. data/yard-stubs/ca_obj_roll.rb +0 -1
  178. data/yard-stubs/ca_obj_stride.rb +10 -7
  179. data/yard-stubs/ca_obj_transpose.rb +1 -1
  180. data/yard-stubs/ca_obj_window.rb +1 -2
  181. data/yard-stubs/carray_access.rb +5 -4
  182. data/yard-stubs/carray_broadcast.rb +1 -1
  183. data/yard-stubs/carray_cast.rb +78 -43
  184. data/yard-stubs/carray_conversion.rb +13 -1
  185. data/yard-stubs/carray_copy.rb +8 -3
  186. data/yard-stubs/carray_core.rb +3 -3
  187. data/yard-stubs/carray_count.rb +9 -0
  188. data/yard-stubs/carray_lazy.rb +5 -1
  189. data/yard-stubs/carray_loop.rb +4 -1
  190. data/yard-stubs/carray_mask.rb +34 -10
  191. data/yard-stubs/carray_math.rb +25 -13
  192. data/yard-stubs/carray_median_percentile.rb +10 -6
  193. data/yard-stubs/carray_order.rb +34 -13
  194. data/yard-stubs/carray_scatter.rb +10 -3
  195. data/yard-stubs/carray_segment.rb +72 -0
  196. data/yard-stubs/carray_slab.rb +2 -1
  197. data/yard-stubs/carray_sort.rb +43 -3
  198. data/yard-stubs/carray_test.rb +10 -0
  199. data/yard-stubs/carray_utils.rb +0 -26
  200. metadata +11 -19
  201. data/ext/ca_for_each_element.h +0 -239
  202. data/ext/carray_build_flags.h +0 -3
  203. data/ext/carray_config.h +0 -28
  204. data/ext/carray_kernels_bincmp.c +0 -4446
  205. data/ext/carray_kernels_binop.c +0 -11001
  206. data/ext/carray_kernels_init.c +0 -1131
  207. data/ext/carray_kernels_map.c +0 -3467
  208. data/ext/carray_kernels_moncmp.c +0 -2097
  209. data/ext/carray_kernels_monop.c +0 -18313
  210. data/ext/carray_kernels_reduce_aggregate.c +0 -26005
  211. data/ext/carray_kernels_reduce_boolean.c +0 -330
  212. data/ext/carray_kernels_reduce_cumulative.c +0 -14862
  213. data/ext/carray_kernels_reduce_extreme.c +0 -17494
  214. data/ext/carray_kernels_reduce_variance.c +0 -3910
  215. data/ext/carray_kernels_scan.c +0 -3693
  216. data/ext/carray_kernels_search.c +0 -32218
  217. data/ext/carray_kernels_sort.c +0 -10626
  218. data/ext/carray_kernels_triop.c +0 -1392
@@ -167,19 +167,44 @@ class CABlockIterator < CAIterator
167
167
  # below a full tile, so `min_count:` marks them UNDEF with no masking. The
168
168
  # output data type is seeded from the first region (all regions share it).
169
169
  def assemble
170
- outs = nil
170
+ grids = nil
171
+ faces = nil
172
+ single = true
171
173
  each_region do |strip_ranges, tiles, out_ranges|
172
174
  view = @source[*strip_ranges].block_view(*tiles)
173
175
  res = yield(view, tiles)
174
- if res.is_a?(Array)
175
- outs ||= res.map { |r| CArray.new(r.data_type, @shape) }
176
- res.each_index { |k| outs[k][*out_ranges] = res[k] }
177
- else
178
- outs ||= CArray.new(res.data_type, @shape)
179
- outs[*out_ranges] = res
180
- end
176
+ single = false if res.is_a?(Array)
177
+ res = [res] unless res.is_a?(Array)
178
+ faces ||= res.map { |r| r.face? ? r : nil }
179
+ parts = res.map { |r| r.face? ? face_grid_part(r) : r }
180
+ grids ||= parts.map { |r| CArray.new(r.data_type, @shape, bytes: r.bytes) }
181
+ parts.each_index { |k| grids[k][*out_ranges] = parts[k] }
181
182
  end
182
- outs
183
+ unless grids
184
+ # An empty source has no region. A view with unit tiles has no cells
185
+ # either; reducing it gives each output's type for the empty grid.
186
+ res = yield(@source.block_view(*[1] * @sndim), [1] * @sndim)
187
+ single = !res.is_a?(Array)
188
+ res = [res] unless res.is_a?(Array)
189
+ faces = res.map { |r| r.face? ? r : nil }
190
+ grids = res.map { |r| r = face_grid_part(r) if r.face?; CArray.new(r.data_type, @shape, bytes: r.bytes) }
191
+ end
192
+ outs = grids.each_index.map { |k| faces[k] ? face_grid(grids[k], faces[k]) : grids[k] }
193
+ single ? outs[0] : outs
194
+ end
195
+
196
+ # A region result that is a Face (a CATime mean, a CATimedelta stddev) is
197
+ # assembled on its storage and the Face is put back on the whole grid;
198
+ # assembling the Face itself would copy its surface bytes into an array that
199
+ # no longer knows it is one. A CAConstString's storage points into its own
200
+ # buffer, which the regions do not share, so it is assembled as strings.
201
+ def face_grid_part (r)
202
+ r.is_a?(CAConstString) ? r.to_string.parent : r.parent
203
+ end
204
+
205
+ def face_grid (grid, face)
206
+ return CArray.const_string(grid, encoding: face.encoding) if face.is_a?(CAConstString)
207
+ grid.face_lift(face)
183
208
  end
184
209
 
185
210
  # Fold every region with a single core reduction `op` (the tier-1 shape).
@@ -255,19 +280,13 @@ class CABlockIterator < CAIterator
255
280
  # @param fill_value [Object, nil] value to put in place of a masked
256
281
  # result instead of leaving it masked.
257
282
  # @return [CArray] tile-grid shaped
258
- # @!method all(min_count: nil, fill_value: nil)
283
+ # @!method all
259
284
  # Whether every cell of each tile is true.
260
- # @param min_count [Integer, nil] fewest cells that must be present
261
- # for a result; a piece with fewer comes back masked.
262
- # @param fill_value [Object, nil] value to put in place of a masked
263
- # result instead of leaving it masked.
285
+ # Takes no `min_count:` or `fill_value:`, as the core `all` does not.
264
286
  # @return [CArray] tile-grid shaped
265
- # @!method any(min_count: nil, fill_value: nil)
287
+ # @!method any
266
288
  # Whether any cell of each tile is true.
267
- # @param min_count [Integer, nil] fewest cells that must be present
268
- # for a result; a piece with fewer comes back masked.
269
- # @param fill_value [Object, nil] value to put in place of a masked
270
- # result instead of leaving it masked.
289
+ # Takes no `min_count:` or `fill_value:`, as the core `any` does not.
271
290
  # @return [CArray] tile-grid shaped
272
291
  # @!method variancep(min_count: nil, fill_value: nil)
273
292
  # Per-tile population variance (divisor `n`).
@@ -283,9 +302,12 @@ class CABlockIterator < CAIterator
283
302
  # @param fill_value [Object, nil] value to put in place of a masked
284
303
  # result instead of leaving it masked.
285
304
  # @return [CArray] tile-grid shaped
286
- [:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev, :all, :any,
305
+ [:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev,
287
306
  :variancep, :stddevp].each do |op|
288
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
289
311
  kw = {}
290
312
  kw[:min_count] = min_count unless min_count.nil?
291
313
  kw[:fill_value] = fill_value unless fill_value.nil?
@@ -293,6 +315,17 @@ class CABlockIterator < CAIterator
293
315
  end
294
316
  end
295
317
 
318
+ [:all, :any].each do |op|
319
+ define_method(op) { |**kw| fold(op, **CAIterator.reduce_keywords(op, kw)) }
320
+ end
321
+
322
+ # A Face's minimum and maximum are cells of the Face, so they are gathered
323
+ # from the source by the winner's address. Assembling the per-region results
324
+ # instead copies the Face's storage into an array that no longer knows it is
325
+ # one: a CATime came back as 8-byte strings.
326
+ FACE_WINNER_INDEX = { min: :min_index, max: :max_index }.freeze
327
+ private_constant :FACE_WINNER_INDEX
328
+
296
329
  # ---- count family and elements ----------------------------------------
297
330
 
298
331
  # @overload count(v = <none>)
@@ -300,15 +333,16 @@ class CABlockIterator < CAIterator
300
333
  # partial edge tile); `count(UNDEF)` counts masked cells; `count(v)` counts
301
334
  # cells equal to `v`.
302
335
  # @return [CArray] tile-grid shaped
303
- def count (*args)
304
- return count_not_masked if args.empty?
336
+ def count (*args, **kw)
337
+ return count_not_masked(**kw) if args.empty?
338
+ kw = CAIterator.reduce_keywords(:count, kw)
305
339
  out = nil
306
340
  each_region do |strip_ranges, tiles, out_ranges|
307
341
  view = @source[*strip_ranges].block_view(*tiles)
308
342
  # block_view is a CAStride, so #count is not shadowed; dispatch
309
343
  # CArray#count explicitly anyway, matching the family regularity.
310
- red = CArray.instance_method(:count).bind_call(view, *args, axis: @tile_axes)
311
- out ||= CArray.new(red.data_type, @shape)
344
+ red = CArray.instance_method(:count).bind_call(view, *args, axis: @tile_axes, **kw)
345
+ out ||= CArray.new(red.data_type, @shape, bytes: red.bytes)
312
346
  out[*out_ranges] = red
313
347
  end
314
348
  out
@@ -318,16 +352,21 @@ class CABlockIterator < CAIterator
318
352
  # Per-tile count of present (non-masked) cells. For a partial edge tile
319
353
  # this is the real cell count (the OOB cells are not present).
320
354
  # @return [CArray]
321
- def count_not_masked
322
- fold(:count_not_masked)
355
+ def count_not_masked (**kw)
356
+ fold(:count_not_masked, **CAIterator.reduce_keywords(:count_not_masked, kw))
323
357
  end
324
358
 
325
359
  # @overload count_masked
326
360
  # Per-tile count of masked cells. Equals `elements - count_not_masked`;
327
361
  # the OOB cells of a partial edge tile count here.
328
362
  # @return [CArray]
329
- def count_masked
330
- elements - count_not_masked
363
+ def count_masked (**kw)
364
+ kw = CAIterator.reduce_keywords(:count_masked, kw)
365
+ need = CArray.__min_count_need__(:count_masked, kw[:min_count], 0)
366
+ out = elements - count_not_masked
367
+ # A count of the mask reads every cell of the tile.
368
+ out[elements.lt(need)] = UNDEF if need > 0
369
+ CArray.__reduce_fill__(out, kw[:fill_value])
331
370
  end
332
371
 
333
372
  # @overload elements
@@ -349,6 +388,10 @@ class CABlockIterator < CAIterator
349
388
  # Per-tile `[min, max]` (two tile-grid-shaped CArrays, a single fused pass).
350
389
  # @return [Array<CArray>]
351
390
  def minmax (min_count: nil, fill_value: nil)
391
+ if @source.face?
392
+ return [min(min_count: min_count, fill_value: fill_value),
393
+ max(min_count: min_count, fill_value: fill_value)]
394
+ end
352
395
  kw = {}
353
396
  kw[:min_count] = min_count unless min_count.nil?
354
397
  kw[:fill_value] = fill_value unless fill_value.nil?
@@ -364,8 +407,9 @@ class CABlockIterator < CAIterator
364
407
  # @return [CArray]
365
408
  [:min_index, :max_index].each do |op|
366
409
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
367
- def #{op}
368
- assemble { |view, _| view.#{op}(axis: @tile_axes) }
410
+ def #{op} (**kw)
411
+ kw = CAIterator.reduce_keywords(:#{op}, kw)
412
+ assemble { |view, _| view.#{op}(axis: @tile_axes, **kw) }
369
413
  end
370
414
  RUBY
371
415
  end
@@ -376,26 +420,49 @@ class CABlockIterator < CAIterator
376
420
  # minima. Unlike `min_index` (the tile-local position) this indexes back
377
421
  # into the original array. An all-masked tile is a masked cell.
378
422
  # @return [CArray] tile-grid shaped int64
379
- def min_addr; winner_addr(:min_index); end
423
+ def min_addr (**kw); winner_addr(:min_index, kw); end
380
424
 
381
425
  # @overload max_addr
382
426
  # Per-tile flat source address of the maximum. See {#min_addr}.
383
427
  # @return [CArray] tile-grid shaped int64
384
- def max_addr; winner_addr(:max_index); end
428
+ def max_addr (**kw); winner_addr(:max_index, kw); end
385
429
 
386
430
  private
387
431
 
432
+ # Per-tile minimum or maximum of a Face source, gathered from the source by
433
+ # the winner's address so the result is the Face itself. An all-masked tile,
434
+ # and with `min_count` a tile with too few present cells, is a masked cell.
435
+ def face_winner (idx_op, min_count, fill_value)
436
+ addr = winner_addr(idx_op)
437
+ need = CArray.__min_count_need__(idx_op == :min_index ? :min : :max, min_count, 0)
438
+ addr[count_not_masked.lt(need)] = UNDEF if need > 0
439
+ src = @source.reshape(@source.elements)
440
+ empty = addr.is_masked
441
+ 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
444
+ # carries the mask through the gather, which a read-only Face (whose copy
445
+ # cannot be written) needs. Otherwise mask the placeholder afterwards.
446
+ if fill_value.nil? && src.has_mask? && (hole = src.is_masked.where).elements > 0
447
+ return src[addr.strip_mask(hole[0])].copy
448
+ end
449
+ out = src[addr.strip_mask(0)].copy
450
+ out[empty] = fill_value.nil? ? UNDEF : fill_value
451
+ out
452
+ end
453
+
388
454
  # Per-tile source address of the winner. The tile-local flat index (min_index /
389
455
  # max_index) is looked up in a source-address companion tiled the same way, so
390
456
  # the tile geometry is reused rather than recomputed. An all-masked tile yields
391
457
  # a masked index; it is gathered at 0 and re-masked in the result.
392
- def winner_addr (idx_op)
458
+ def winner_addr (idx_op, kw = {})
459
+ kw = CAIterator.reduce_keywords(idx_op, kw)
393
460
  saddr = CArray.int64(*@source.shape).seq!
394
461
  out = nil
395
462
  each_region do |strip_ranges, tiles, out_ranges|
396
463
  vview = @source[*strip_ranges].block_view(*tiles)
397
464
  sview = saddr[*strip_ranges].block_view(*tiles)
398
- mi = vview.public_send(idx_op, axis: @tile_axes) # tile-local flat index
465
+ mi = vview.public_send(idx_op, axis: @tile_axes, **kw.slice(:min_count)) # tile-local flat index
399
466
  grid = (0...@sndim).map { |i| vview.shape[i] }
400
467
  cells = tiles.inject(1) { |p, t| p * t }
401
468
  safe = mi.copy
@@ -407,7 +474,7 @@ class CABlockIterator < CAIterator
407
474
  out ||= CArray.int64(*@shape)
408
475
  out[*out_ranges] = picked
409
476
  end
410
- out
477
+ CArray.__reduce_fill__(out || CArray.int64(*@shape), kw[:fill_value])
411
478
  end
412
479
 
413
480
  public
@@ -441,15 +508,17 @@ class CABlockIterator < CAIterator
441
508
  # Per-tile weighted sum; `weights` is shaped like one full tile (`Π b_i`).
442
509
  # At a partial edge tile the weight kernel is sliced to the present cells.
443
510
  # @return [CArray]
444
- def wsum (weights)
445
- weighted(weights) { |view, w| view.wsum(w, axis: @tile_axes) }
511
+ def wsum (weights, **kw)
512
+ kw = CAIterator.reduce_keywords(:wsum, kw)
513
+ weighted(weights) { |view, w| view.wsum(w, axis: @tile_axes, **kw) }
446
514
  end
447
515
 
448
516
  # @overload wmean(weights)
449
517
  # Per-tile weighted mean; `weights` shaped like one full tile.
450
518
  # @return [CArray]
451
- def wmean (weights)
452
- weighted(weights) { |view, w| view.wmean(w, axis: @tile_axes) }
519
+ def wmean (weights, **kw)
520
+ kw = CAIterator.reduce_keywords(:wmean, kw)
521
+ weighted(weights) { |view, w| view.wmean(w, axis: @tile_axes, **kw) }
453
522
  end
454
523
 
455
524
  private
@@ -479,21 +548,22 @@ class CABlockIterator < CAIterator
479
548
  # Core per-axis order statistics take a single integer axis, so a 1-D tile
480
549
  # delegates straight to the C path; a multi-axis tile is materialized per
481
550
  # region and its tile axes flattened into one before the single-axis core
482
- # order-stat. A masked source raises (the known per-axis masked limitation);
483
- # strip the mask with `ca.value` first if needed.
551
+ # order-stat. Masked cells are left out, as in `CArray#median(axis:)`.
484
552
 
485
553
  # @overload median
486
554
  # Per-tile median. @return [CArray]
487
- def median
488
- order_stat { |v, axis| v.median(axis: axis) }
555
+ def median (**kw)
556
+ kw = CAIterator.reduce_keywords(:median, kw)
557
+ order_stat { |v, axis| v.median(axis: axis, **kw) }
489
558
  end
490
559
 
491
560
  # @overload percentile(*pers)
492
561
  # Per-tile percentile(s). One argument returns one CArray, several an array
493
562
  # of CArrays (as `CArray#percentile`).
494
563
  # @return [CArray, Array<CArray>]
495
- def percentile (*pers)
496
- order_stat { |v, axis| v.percentile(*pers, axis: axis) }
564
+ def percentile (*pers, **kw)
565
+ kw = CAIterator.reduce_keywords(:percentile, kw)
566
+ order_stat { |v, axis| v.percentile(*pers, axis: axis, **kw) }
497
567
  end
498
568
 
499
569
  # @overload quantile
@@ -585,7 +655,7 @@ class CABlockIterator < CAIterator
585
655
  # @return [CArray, Enumerator] source-shaped
586
656
  def map
587
657
  return to_enum(:map) unless block_given?
588
- pout = CArray.new(padded_source.data_type, padded_source.shape)
658
+ pout = CArray.new(padded_source.data_type, padded_source.shape, bytes: padded_source.bytes)
589
659
  pgv = pout.block_view(*@sizes)
590
660
  tgv = tile_grid_view
591
661
  nils = Array.new(@sndim, nil)
@@ -640,7 +710,7 @@ class CABlockIterator < CAIterator
640
710
  tile = tgv[*g, *nils]
641
711
  scanned = tile.copy.reshape(tile.elements).public_send(op).reshape(*@sizes)
642
712
  unless pout
643
- pout = CArray.new(scanned.data_type, padded_source.shape)
713
+ pout = CArray.new(scanned.data_type, padded_source.shape, bytes: scanned.bytes)
644
714
  pout[] = UNDEF
645
715
  pgv = pout.block_view(*@sizes)
646
716
  end
@@ -659,7 +729,7 @@ class CABlockIterator < CAIterator
659
729
  if pshape == @source.shape.to_a
660
730
  @source
661
731
  else
662
- pad = CArray.new(@source.data_type, pshape)
732
+ pad = CArray.new(@source.data_type, pshape, bytes: @source.bytes)
663
733
  pad[] = UNDEF
664
734
  pad[*@sndim.times.map { |i| 0...@source.shape[i] }] = @source
665
735
  pad
@@ -69,41 +69,33 @@ class CArray
69
69
  # @return [Boolean, CArray] a scalar, or an array when an axis is given.
70
70
  def none (skip_masked: true, **opts)
71
71
  return __none_skipna__(**opts) if skip_masked
72
- # none = not any (Kleene): not(true)=false, not(false)=true, not(UNDEF)=UNDEF
73
- r = __kleene_fold(:any, opts)
74
- r.is_a?(CArray) ? r.not : (r.equal?(UNDEF) ? UNDEF : !r)
72
+ __kleene_fold(:none, opts)
75
73
  end
76
74
 
77
75
  private
78
76
 
79
- # Three-valued fold via counts. `kind` is :any (OR) or :all (AND).
80
- # `opts` carries axis: / keep_axis: (same surface as the skipna reduction).
77
+ # Three-valued fold. `kind` is :any, :all or :none. The available-case
78
+ # reduction answers wherever an unmasked cell settles the fold (a true for
79
+ # any, a false for all, a true for none); elsewhere a masked cell leaves the
80
+ # answer undetermined. Going through that reduction checks the data type,
81
+ # the axes and the keywords exactly as `skip_masked: true` does.
81
82
  def __kleene_fold (kind, opts)
82
- axis = opts[:axis]
83
- if axis.nil?
84
- if kind == :any
85
- return true if count(true) > 0 # a known TRUE settles OR
86
- else
87
- return false if count(false) > 0 # a known FALSE settles AND
88
- end
89
- return UNDEF if count_masked > 0 # undetermined
90
- return( kind == :any ? false : true )
83
+ r = case kind
84
+ when :any then __any_skipna__(**opts)
85
+ when :all then __all_skipna__(**opts)
86
+ else __none_skipna__(**opts)
87
+ end
88
+ cm = count_masked(**opts)
89
+ if r.is_a?(CArray)
90
+ settled = ( kind == :any ) ? r : r.not
91
+ out = r.copy
92
+ out[settled.not & (cm > 0)] = UNDEF
93
+ out
94
+ else
95
+ settled = ( kind == :any ) ? r : !r
96
+ return r if settled
97
+ ( cm > 0 ) ? UNDEF : r
91
98
  end
92
-
93
- keep = opts[:keep_axis]
94
- cnt_kw = keep ? { axis: axis, keep_axis: keep } : { axis: axis }
95
- ck = ( kind == :any ) ? count(true, **cnt_kw) : count(false, **cnt_kw)
96
- cm = count_masked(axis: axis) # count_masked has no keep_axis
97
- cm = cm.reshape(*ck.shape) unless cm.shape == ck.shape
98
-
99
- # A cell is undetermined only where it is masked AND the known side does
100
- # not settle it (ck counts the settling value: true for OR, false for AND).
101
- # Masking only those cells avoids leaving an all-zero mask behind.
102
- out = CArray.boolean(*ck.shape)
103
- out[] = ( kind == :any ) ? 0 : 1 # OR default false / AND default true
104
- out[ck.eq(0) & (cm > 0)] = UNDEF # genuinely undetermined
105
- out[ck > 0] = ( kind == :any ) ? 1 : 0 # known side dominates
106
- out
107
99
  end
108
100
 
109
101
  end
@@ -297,7 +297,7 @@ class CACategorical < CAObject
297
297
 
298
298
  # Face hook: decode a per-cell code into its category label, through the one
299
299
  # decode every code-to-label path shares (see #label_at).
300
- def storage_to_scalar(raw)
300
+ def storage_to_element(raw)
301
301
  code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw
302
302
  label_at(code)
303
303
  end
@@ -317,6 +317,27 @@ class CACategorical < CAObject
317
317
  # an all-false result.
318
318
  # @param label [Object] category to match.
319
319
  # @return [CArray]
320
+ # @overload resize(*newdim, fill_value: UNDEF)
321
+ # Returns a categorical resized to `newdim` (see {CArray#resize}).
322
+ # The new cells are excluded (UNDEF) unless `fill_value` names one of
323
+ # the labels, since a zero code would read as the first label.
324
+ # @raise [ArgumentError] when `fill_value` is not a label.
325
+ def resize (*newdim, fill_value: UNDEF)
326
+ CACategorical.from_codes(codes.resize(*newdim, fill_value: code_for_fill(fill_value)),
327
+ labels)
328
+ end
329
+
330
+ # @overload insert_block(offset, bsize, &block)
331
+ # Returns a categorical with a block inserted (see
332
+ # {CArray#insert_block}). The inserted cells are excluded (UNDEF)
333
+ # unless the block returns one of the labels.
334
+ # @raise [ArgumentError] when the block returns something that is not
335
+ # a label.
336
+ def insert_block (offset, bsize, &block)
337
+ fill = block ? code_for_fill(block.call) : UNDEF
338
+ CACategorical.from_codes(codes.insert_block(offset, bsize) { fill }, labels)
339
+ end
340
+
320
341
  def eq(label)
321
342
  codes.eq(@labels.index(label) || @labels.size)
322
343
  end
@@ -356,11 +377,11 @@ class CACategorical < CAObject
356
377
  # so `labels.zip(category_sizes.to_a)` always pairs up.
357
378
  # @return [CArray]
358
379
  def category_sizes
359
- return @_category_sizes if @_category_sizes
380
+ return @category_sizes if @category_sizes
360
381
  bc = codes.bincount
361
- out = CArray.new(bc.data_type, [@labels.size]) # new zero-fills
382
+ out = CArray.new(bc.data_type, [@labels.size], bytes: bc.bytes) # new zero-fills
362
383
  out[0...bc.elements] = bc if bc.elements > 0
363
- @_category_sizes = out
384
+ @category_sizes = out
364
385
  out
365
386
  end
366
387
 
@@ -463,6 +484,14 @@ class CACategorical < CAObject
463
484
  # that has none. Every code-to-label path goes through here rather than
464
485
  # indexing @labels itself, because two decodes are two chances to disagree,
465
486
  # and disagreeing is how #unique came to contradict #to_a on the same cell.
487
+ # The code a fill value stands for: UNDEF stays UNDEF, a label is its code.
488
+ def code_for_fill (value)
489
+ return UNDEF if UNDEF.equal?(value)
490
+ code = labels.index(value)
491
+ raise ArgumentError, "#{value.inspect} is not one of the labels #{labels.inspect}" unless code
492
+ code
493
+ end
494
+
466
495
  def label_at (code)
467
496
  (code < 0 || code >= @labels.size) ? nil : @labels[code]
468
497
  end
@@ -541,13 +570,11 @@ class CACategorical < CAObject
541
570
  # addresses that order the cells by category (excluded cells last).
542
571
  # @return [CArray]
543
572
  def sort_addr
544
- return @_sort_addr if @_sort_addr
573
+ return @sort_addr if @sort_addr
545
574
  n = elements
546
575
  k = @labels.size
547
- cs = category_sizes # per-category counts
548
- nvalid = cs.sum
549
- cur = CArray.int64(k > 0 ? k : 0) # segment starts, consumed as cursor
550
- cur[1..-1] = cs.cumsum.int64[0..-2] if k > 1
576
+ nvalid = category_sizes.sum
577
+ cur = reduceat_index.copy # segment starts, consumed as cursor
551
578
  flat = codes.reshape(n)
552
579
  seq = CArray.int64(n).seq! # source indices, scattered as payload
553
580
  out = CArray.int64(n)
@@ -566,7 +593,7 @@ class CACategorical < CAObject
566
593
  excluded = excluded.or(flat.is_masked) if flat.has_mask?
567
594
  out[nvalid..-1] = seq[excluded] # excluded cells, source order
568
595
  end
569
- @_sort_addr = out
596
+ @sort_addr = out
570
597
  out
571
598
  end
572
599
 
@@ -581,17 +608,26 @@ class CACategorical < CAObject
581
608
  # offsets aligned to {#labels}. Pair with {#category_sizes} for lengths.
582
609
  # @return [CArray]
583
610
  def reduceat_index
584
- return @_reduceat_index if @_reduceat_index
585
- k = @labels.size
586
- out = CArray.int64(k > 0 ? k : 0)
587
- if k > 1
588
- out[0] = 0
589
- out[1..-1] = category_sizes.cumsum.int64[0..-2]
590
- elsif k == 1
591
- out[0] = 0
611
+ return @reduceat_index if @reduceat_index
612
+ # Every boundary but the last; the copy makes it an entity the
613
+ # reduceat kernels can read directly.
614
+ @reduceat_index = CArray.segment_offsets(lengths: category_sizes)[0...-1].copy
615
+ end
616
+
617
+ # @overload categorize(labels: nil, sort_labels: false)
618
+ # Returns a {CACategorical} keyed on the labels, as categorizing any
619
+ # key array does: the labels that occur, in first-appearance order (or
620
+ # sorted with `sort_labels:`), or the fixed `labels:` vocabulary. A
621
+ # category of `self` with no cells does not carry over; a masked cell
622
+ # is excluded.
623
+ # @return [CACategorical]
624
+ def categorize (labels: nil, sort_labels: false)
625
+ if labels || sort_labels
626
+ return label_values.categorize(labels: labels, sort_labels: sort_labels)
592
627
  end
593
- @_reduceat_index = out
594
- out
628
+ # Distinctness rides the codes; only the k labels cross the surface.
629
+ recoded = codes.categorize
630
+ CACategorical.from_codes(recoded.codes, recoded.labels.map { |c| label_at(c) })
595
631
  end
596
632
 
597
633
  # @overload inspect
@@ -629,6 +665,24 @@ class CArray
629
665
  # @return [CACategorical]
630
666
  # @raise [ArgumentError] when explicit `labels` contain
631
667
  # duplicates.
668
+ # @raise [CArray::DataTypeError] when `sort_labels: true` discovers
669
+ # the labels of a complex array, which have no order.
670
+ # The order sort_labels: puts two labels in: as sort orders the cells of an
671
+ # array, a Float NaN after every other value; two labels <=> cannot order
672
+ # are refused, by name.
673
+ private def label_order(a, b)
674
+ na = a.is_a?(Float) && a.nan?
675
+ nb = b.is_a?(Float) && b.nan?
676
+ return (na ? 1 : 0) - (nb ? 1 : 0) if na || nb
677
+ c = a <=> b
678
+ if c.nil?
679
+ raise ArgumentError,
680
+ "categorize(sort_labels: true): cannot order the labels " \
681
+ "#{a.inspect} and #{b.inspect}"
682
+ end
683
+ c
684
+ end
685
+
632
686
  def categorize(labels: nil, sort_labels: false)
633
687
  # Automatic appearance-order vocabulary: one linear pass (C
634
688
  # __factorize_appearance__) returns both codes and levels directly, over the
@@ -636,12 +690,26 @@ class CArray
636
690
  # lane). Distinctness is the hash-key judgement shared with the discovery
637
691
  # family: Float NaN collapses to one category and -0.0 == +0.0, while mixed
638
692
  # Integer / Float keys stay distinct (eql?, so 1 and 1.0 are separate
639
- # categories). The discovery path below is reserved for sort_labels (which
640
- # reorders the vocabulary, desyncing the appearance-order codes), an explicit
641
- # labels list, and the data types the factorize kernel does not take (complex).
642
- if labels.nil? && !sort_labels && (integer? || float? || object? || fixlen? || boolean?)
693
+ # categories). Complex values are distinct by value with the same NaN and
694
+ # -0.0 rules, and have no order, so sort_labels refuses them. The discovery
695
+ # path below is reserved for an explicit labels list.
696
+ if labels.nil? && sort_labels && complex?
697
+ raise CArray::DataTypeError,
698
+ "categorize(sort_labels: true): a #{data_type_name} value has no " \
699
+ "order (complex numbers are not ordered); without sort_labels: the " \
700
+ "labels are in first-appearance order"
701
+ end
702
+ if labels.nil? && (integer? || float? || complex? || object? || fixlen? || boolean?)
643
703
  codes, levels = __factorize_appearance__
644
- return CACategorical.from_codes(codes, levels.to_a)
704
+ levels = levels.to_a
705
+ return CACategorical.from_codes(codes, levels) unless sort_labels
706
+ # Sort the k levels as Ruby sorts them, then renumber every code through
707
+ # the rank in one gather. The sentinel of an excluded cell is out of the
708
+ # rank's range, so project makes it UNDEF and from_codes writes it back.
709
+ order = (0...levels.size).sort { |i, j| label_order(levels[i], levels[j]) }
710
+ rank = CArray.new(codes.data_type, [levels.size])
711
+ order.each_with_index { |old, new_code| rank[old] = new_code }
712
+ return CACategorical.from_codes(rank.project(codes), order.map { |i| levels[i] })
645
713
  end
646
714
 
647
715
  if labels.nil?
@@ -650,7 +718,7 @@ class CArray
650
718
  # masked keys stay excluded), so the non-masked cells are the levels.
651
719
  # Only the final list is Ruby, since labels are Ruby objects.
652
720
  labels_arr = mask_duplicates[:is_not_masked].to_a
653
- labels_arr.sort! if sort_labels
721
+ labels_arr.sort! { |a, b| label_order(a, b) } if sort_labels
654
722
  else
655
723
  labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels)
656
724
  if labels_arr.uniq.size != labels_arr.size