carray 3.0.1 → 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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  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 +90 -41
  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 +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  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 +194 -42
  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 +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -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 +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  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 +551 -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 +88 -75
  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 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  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 +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. data/ext/carray_kernels_triop.c +0 -1392
@@ -36,7 +36,7 @@ class CAMeld
36
36
 
37
37
  def sum(*args, **kw)
38
38
  return super unless args.empty? && meld_reduce_fast_path_ok?(kw)
39
- axis = kw[:axis]
39
+ axis = meld_fast_path_axis(kw)
40
40
  if axis.nil?
41
41
  parents.map(&:sum).inject(:+)
42
42
  elsif meld_axis_normalized?(axis)
@@ -52,7 +52,10 @@ class CAMeld
52
52
  # @return [CArray, Numeric]
53
53
  def mean(*args, **kw)
54
54
  return super unless args.empty? && meld_reduce_fast_path_ok?(kw)
55
- axis = kw[:axis]
55
+ # The combine divides in Float; an object array's mean stays exact
56
+ # (Rational, Integer division) on the core path.
57
+ return super if object?
58
+ axis = meld_fast_path_axis(kw)
56
59
  # `mean` has no identity: an empty set of contributors has no defined
57
60
  # mean (0/0 = NaN). Punt to super, which returns UNDEF: a reduction
58
61
  # with no contributors yields the identity where one exists and UNDEF
@@ -78,12 +81,12 @@ class CAMeld
78
81
  # @return [CArray, Numeric]
79
82
  def min(*args, **kw)
80
83
  return super unless args.empty? && meld_reduce_fast_path_ok?(kw)
81
- axis = kw[:axis]
84
+ axis = meld_fast_path_axis(kw)
82
85
  # `min` has no identity — empty parent list / all-empty parents punt to
83
86
  # super for UNDEF.
84
87
  if axis.nil?
85
88
  return super if parents.all? { |p| p.elements == 0 }
86
- parents.reject { |p| p.elements == 0 }.map(&:min).min
89
+ meld_combine_flat(:min)
87
90
  elsif meld_axis_normalized?(axis)
88
91
  return super if parents.all? { |p| p.dim[meld_axis] == 0 }
89
92
  nonempty = parents.reject { |p| p.dim[meld_axis] == 0 }
@@ -99,10 +102,10 @@ class CAMeld
99
102
  # @return [CArray, Numeric]
100
103
  def max(*args, **kw)
101
104
  return super unless args.empty? && meld_reduce_fast_path_ok?(kw)
102
- axis = kw[:axis]
105
+ axis = meld_fast_path_axis(kw)
103
106
  if axis.nil?
104
107
  return super if parents.all? { |p| p.elements == 0 }
105
- parents.reject { |p| p.elements == 0 }.map(&:max).max
108
+ meld_combine_flat(:max)
106
109
  elsif meld_axis_normalized?(axis)
107
110
  return super if parents.all? { |p| p.dim[meld_axis] == 0 }
108
111
  nonempty = parents.reject { |p| p.dim[meld_axis] == 0 }
@@ -168,6 +171,9 @@ class CAMeld
168
171
  # more elaborate (min_count / fill_value / keep_axis / multi-axis /
169
172
  # mask propagation) punts to super.
170
173
  def meld_reduce_fast_path_ok?(kw)
174
+ return false unless CArray.__fast_paths_enabled__
175
+ # The mask of a meld is a meld built in C, with no parents list.
176
+ return false unless parents
171
177
  return false unless (kw.keys - [:axis]).empty?
172
178
  axis = kw[:axis]
173
179
  unless axis.nil?
@@ -177,9 +183,30 @@ class CAMeld
177
183
  end
178
184
  return false if has_mask?
179
185
  parents.each { |p| return false if p.has_mask? }
186
+ # A meld of Face arrays keeps its parents in their Face; the per-parent
187
+ # calls would reach the Face's own reductions, which answer in the Face's
188
+ # terms (or refuse), not in storage.
189
+ parents.each { |p| return false if p.face? }
180
190
  true
181
191
  end
182
192
 
193
+ # The axis a fast path reduces along. A 1-D meld has only the meld axis,
194
+ # and reducing it is reducing the whole array: the answer is a scalar, as
195
+ # for the flat call.
196
+ def meld_fast_path_axis(kw)
197
+ ndim == 1 ? nil : kw[:axis]
198
+ end
199
+
200
+ # The per-parent answers of a whole-array min / max, combined by core's
201
+ # own min / max so NaN loses the way it loses inside one parent (Ruby's
202
+ # Array#min would raise on it).
203
+ def meld_combine_flat(op)
204
+ vals = parents.reject { |p| p.elements == 0 }.map(&op)
205
+ part = CArray.new(data_type, [vals.size])
206
+ vals.each_with_index { |v, i| part[i] = v }
207
+ part.public_send(op)
208
+ end
209
+
183
210
  # Non-meld-axis decompose: each parent reduces the same axis independently,
184
211
  # then the K per-parent results are concatenated back along the meld axis.
185
212
  # Materialised to entity (`.copy`) to match CArray#reduce entity semantics.
@@ -198,7 +225,13 @@ class CAMeld
198
225
 
199
226
  def variance_family(args, kw, sample:, sqrt:)
200
227
  return yield unless args.empty? && meld_reduce_fast_path_ok?(kw)
201
- axis = kw[:axis]
228
+ # The merge works in Float; an object array's variance stays exact
229
+ # (Rational, BigDecimal) on the core path.
230
+ return yield if object?
231
+ # The merge squares the difference of means, which for complex values is
232
+ # not |δ|²; the core path computes the variance of a complex array.
233
+ return yield if complex?
234
+ axis = meld_fast_path_axis(kw)
202
235
  # Empty-parent / short-parent handling — Welford needs n >= 2 per parent
203
236
  # to recover m2 from p.variance for sample, n >= 1 for variancep (n=1
204
237
  # gives m2=0, fine). Any parent below its threshold punts to super,
@@ -85,8 +85,10 @@ class CArray
85
85
  # b_on_grid = b_data.project(ib)
86
86
  #
87
87
  # `direction:` (`:round` / `:floor` / `:ceil`) and `tolerance:` are
88
- # forwarded to {#locate_nearest_addr}: out-of-range grid points, and points
89
- # whose nearest value is farther than `tolerance`, come back masked.
88
+ # forwarded to {#locate_nearest_addr}: grid points with no match in the
89
+ # direction (before an array's first value under `:floor`, after its last
90
+ # under `:ceil`), and points whose match is farther than `tolerance`, come
91
+ # back masked.
90
92
  #
91
93
  # @param arrays [Array<CArray>] one or more arrays (Array / Range coerced
92
94
  # via `to_ca`).
@@ -94,8 +96,10 @@ class CArray
94
96
  # `nil` uses the first array verbatim.
95
97
  # @param direction [:round, :floor, :ceil] rounding rule for the nearest
96
98
  # match.
97
- # @param tolerance [Numeric, nil] maximum accepted distance; farther grid
98
- # points are masked. `nil` disables the check.
99
+ # @param tolerance [Numeric, String, CATimedelta::Element, nil] maximum
100
+ # accepted distance; farther grid points are masked. For time keys it
101
+ # is a duration (`"10 minutes"` or a CATimedelta value), as in
102
+ # {#locate_nearest_addr}. `nil` disables the check.
99
103
  # @return [Array<CArray>] `[common, idx_0, idx_1, ...]`.
100
104
  # @raise [ArgumentError] when no array is given (or `direction` is invalid,
101
105
  # raised by {#locate_nearest_addr}).
@@ -38,7 +38,7 @@ class CArray
38
38
 
39
39
  if elements.zero?
40
40
  if weights
41
- out = CArray.new(weights.data_type, [length])
41
+ out = CArray.new(weights.data_type, [length], bytes: weights.bytes)
42
42
  else
43
43
  out_type = (length > 0xFFFFFFFF) ? CA_UINT64 : CA_UINT32
44
44
  out = CArray.new(out_type, [length])
@@ -54,7 +54,7 @@ class CArray
54
54
  # Every cell is masked: no labels to count, same result as an empty
55
55
  # input (all-zero output of the requested minimum length).
56
56
  if weights
57
- out = CArray.new(weights.data_type, [length])
57
+ out = CArray.new(weights.data_type, [length], bytes: weights.bytes)
58
58
  else
59
59
  out_type = (length > 0xFFFFFFFF) ? CA_UINT64 : CA_UINT32
60
60
  out = CArray.new(out_type, [length])
@@ -58,9 +58,12 @@ class CArray
58
58
  # multiple of 8) are zero-filled. The byte order matches the packed-bit
59
59
  # convention used by Apache Arrow validity bitmaps and PEP 3118 `?` /
60
60
  # `_Bool` at the bit level.
61
+ # A masked cell has no bit to pack, so a receiver with masked cells is
62
+ # refused; pack the mask separately with {#validity_bits}.
61
63
  # @return [CArray] uint8 CArray of shape `[ceil(n / 8)]`.
62
- # @raise [ArgumentError] when the receiver is not 1-D or its
63
- # `data_type` is not one of `CA_BOOLEAN` / `CA_UINT8` / `CA_INT8`.
64
+ # @raise [ArgumentError] when the receiver is not 1-D, its `data_type`
65
+ # is not one of `CA_BOOLEAN` / `CA_UINT8` / `CA_INT8`, or it has
66
+ # masked cells.
64
67
  def pack_bits
65
68
  unless data_type == CA_BOOLEAN || data_type == CA_UINT8 || data_type == CA_INT8
66
69
  raise ArgumentError,
@@ -71,7 +74,15 @@ class CArray
71
74
  n_bytes = (n + 7) / 8
72
75
  packed = CArray.uint8(n_bytes) { 0 }
73
76
  return packed if n == 0
74
- packed.bitarray.reshape(-1)[0..n-1] = self
77
+ source = self
78
+ if has_mask?
79
+ if count_masked > 0
80
+ raise ArgumentError,
81
+ "pack_bits: masked cells have no bit to pack; pack the mask with validity_bits"
82
+ end
83
+ source = value
84
+ end
85
+ packed.bitarray.reshape(-1)[0..n-1] = source
75
86
  packed
76
87
  end
77
88
 
@@ -75,7 +75,8 @@ class CArray
75
75
  # Use `CArray.meld` for the uniform-shape view-default counterpart.
76
76
  #
77
77
  # @param list [Array<CArray>] pieces to concatenate.
78
- # @param axis [Integer] axis to concatenate along.
78
+ # @param axis [Integer, nil] axis to concatenate along; `nil` is the
79
+ # same as `0`.
79
80
  # @param data_type [Symbol, Integer, nil] result `data_type`;
80
81
  # inferred via `result_type` when `nil`.
81
82
  # @return [CArray] fresh CArray with per-piece `axis` sizes summed.
@@ -83,7 +84,8 @@ class CArray
83
84
  # inconsistent.
84
85
  def self.concatenate (list, axis: 0, data_type: nil)
85
86
  raise ArgumentError, "concatenate: list must not be empty" if list.empty?
86
- __ragged_paste(list, [list.size], axis, data_type)
87
+ axis = 0 if axis.nil?
88
+ __ragged_paste(list, [list.size], axis, data_type, "concatenate")
87
89
  end
88
90
 
89
91
  # @overload concatenate(*others, axis: 0, data_type: nil)
@@ -128,7 +130,7 @@ class CArray
128
130
  raise ArgumentError,
129
131
  "mosaic: tdim product (#{expected}) must equal list size (#{list.size})"
130
132
  end
131
- __ragged_paste(list, tdim, axis, data_type)
133
+ __ragged_paste(list, tdim, axis, data_type, "mosaic")
132
134
  end
133
135
 
134
136
  # Shared eager paste-loop helper (= ex-`combine` paste implementation,
@@ -136,7 +138,7 @@ class CArray
136
138
  # shape obtained by tiling `list` over `tdim` starting at `axis`, with
137
139
  # per-piece dim sizes summed along each tile axis (= block-matrix
138
140
  # consistency required across rows / columns).
139
- def self.__ragged_paste (list, tdim, axis, data_type)
141
+ def self.__ragged_paste (list, tdim, axis, data_type, name)
140
142
  list = CArray.promote_list(list, data_type: data_type)
141
143
  # promote_list has already enforced homogeneity (= common data_type, and
142
144
  # for Face elements: same Face class + portable + state-compatible). So,
@@ -144,12 +146,16 @@ class CArray
144
146
  # result with face_lift, rather than special-casing data_class / DATA_SIZE
145
147
  # here (= which only ever handled CARecord and silently corrupted plain
146
148
  # fixlen / numeric Faces by leaving bytes nil).
147
- face_parent = list[0].face? ? list[0] : nil
149
+ # The Face is re-wrapped from the widest piece: a fixlen Face carries
150
+ # its cell width, and the result is as wide as its widest piece.
151
+ face_parent = list[0].face? ? list.max_by(&:bytes) : nil
148
152
  if face_parent
149
- list = list.map { |x| s = x; s = s.parent while s.face?; s }
153
+ list = list.map { |x| x.__send__(:__face_storage__) }
150
154
  end
151
155
  data_type = list[0].data_type
152
- bytes = (data_type == :fixlen) ? list[0].bytes : nil
156
+ # Fixlen pieces of different widths join at the widest, so no cell is
157
+ # cut; each narrower piece is padded with NUL as it is pasted.
158
+ bytes = (data_type == :fixlen) ? list.map(&:bytes).max : nil
153
159
  # promote_list guarantees a non-empty list of CArrays, so the reference
154
160
  # shape is simply the first element (a CScalar carries shape [1], so a
155
161
  # leading scalar is fine; the scalar-expansion pass below broadcasts it).
@@ -157,7 +163,7 @@ class CArray
157
163
  dim = ref.shape
158
164
  ndim = ref.ndim
159
165
  tndim = tdim.size
160
- axis = CArray.normalize_axis(axis, ndim - tndim + 1, "concatenate/mosaic")
166
+ axis = CArray.normalize_axis(axis, ndim - tndim + 1, name)
161
167
 
162
168
  list = list.map do |x|
163
169
  if x.scalar?
@@ -190,7 +196,7 @@ class CArray
190
196
  block.each_with_index do |item, *tidx|
191
197
  unless item.ndim == ndim
192
198
  raise ArgumentError,
193
- "concatenate/mosaic: piece at tile #{tidx.inspect} has ndim " \
199
+ "#{name}: piece at tile #{tidx.inspect} has ndim " \
194
200
  "#{item.ndim} (expected #{ndim})"
195
201
  end
196
202
  ndim.times do |d|
@@ -199,7 +205,7 @@ class CArray
199
205
  expected = on_tile ? tile_sizes[i][tidx[i]] : dim[d]
200
206
  unless item.shape[d] == expected
201
207
  raise ArgumentError,
202
- "concatenate/mosaic: piece at tile #{tidx.inspect} has " \
208
+ "#{name}: piece at tile #{tidx.inspect} has " \
203
209
  "#{on_tile ? 'tile' : 'non-tile'} axis #{d} size " \
204
210
  "#{item.shape[d]} (expected #{expected}); pieces must agree " \
205
211
  "on non-tile axes and be block-matrix consistent along tile axes"
@@ -0,0 +1,74 @@
1
+ class CArray
2
+
3
+ # The value-hash discovery family (`unique`, `nunique`,
4
+ # `mask_duplicates`, ...) interns one **cell** at a time. `along:`
5
+ # widens the unit to a whole sub-array: the sub-arrays enumerated
6
+ # along one axis, each compared as a single value.
7
+ #
8
+ # Nothing new has to hash. `fz_hash`'s third key lane already interns
9
+ # a fixed-width block of bytes (FNV-1a, with a memcmp re-check), which
10
+ # is what a sub-array is once it sits contiguously; this method is the
11
+ # bridge to it, and the three things the bridge has to get right:
12
+ #
13
+ # - the sub-arrays have to be contiguous before their bytes mean
14
+ # anything, which a transpose or a strided view does not give;
15
+ # - byte equality is not the family's contract for floats, where
16
+ # every NaN is one value and -0.0 is +0.0;
17
+ # - masks fold for free -- a CARefer that covers several parent
18
+ # cells with one view cell OR-reduces their mask bits, so a
19
+ # sub-array holding a masked cell is a masked sub-array and the
20
+ # kernels skip it.
21
+ #
22
+ # Returns a 1-D fixlen array, one cell per sub-array, for the caller
23
+ # to run an ordinary cell-level discovery method over.
24
+ private def fibers_as_cells (along, caller_name)
25
+ if ndim < 2
26
+ raise ArgumentError,
27
+ "#{caller_name}: along: compares sub-arrays, and a #{ndim}-D array " \
28
+ "has none -- drop along: for the whole-array form"
29
+ end
30
+
31
+ if data_type == CA_OBJECT
32
+ raise ArgumentError,
33
+ "#{caller_name}: along: is not available for an object array -- " \
34
+ "its cells hold Ruby references, which would compare by identity " \
35
+ "rather than by value"
36
+ end
37
+
38
+ axis = normalize_axis(along, caller_name)
39
+
40
+ # Bring the named axis to the front so each sub-array is one
41
+ # unbroken run of bytes, and take a copy: `refer` reinterprets a
42
+ # byte buffer, and a view whose element stride is not its element
43
+ # width is not one. The copy is also what makes the normalisation
44
+ # below safe to write.
45
+ order = [axis] + (0...ndim).to_a.reject { |i| i == axis }
46
+ block = (axis.zero? ? self : transpose(*order)).copy
47
+
48
+ # Two values the family calls equal are not equal byte for byte.
49
+ # Normalise them so the bytes say what the values mean.
50
+ if block.float?
51
+ block[:is_nan] = Float::NAN # every NaN is one value
52
+ block[:eq, 0.0] = 0.0 # -0.0 is +0.0
53
+ elsif block.complex?
54
+ [block.real, block.imag].each do |part|
55
+ part[:is_nan] = Float::NAN
56
+ part[:eq, 0.0] = 0.0
57
+ end
58
+ end
59
+
60
+ count = shape[axis]
61
+ block.refer(:fixlen, [count], bytes: elements / count * bytes)
62
+ end
63
+
64
+ # `axis:` and `along:` ask different questions of the same array, so
65
+ # answering both at once has no meaning.
66
+ private def reject_axis_with_along (axis, along, caller_name)
67
+ return unless axis and along
68
+ raise ArgumentError,
69
+ "#{caller_name}: axis: and along: cannot be given together -- " \
70
+ "axis: is about the values inside each sub-array, along: is about " \
71
+ "the sub-arrays themselves"
72
+ end
73
+
74
+ end
@@ -0,0 +1,50 @@
1
+ class CArray
2
+
3
+ # @overload factorize
4
+ # Returns `[codes, levels]` in one pass: `levels` is a 1-D CArray of
5
+ # the distinct values of `self` in first-appearance order — what
6
+ # {#unique} answers — and `codes` is an integer CArray of `self`'s
7
+ # shape where `levels[codes[i]]` is `self[i]`.
8
+ #
9
+ # This is the member of the value-hash discovery family ({#unique},
10
+ # {#value_counts}, {#mask_duplicates}, {#nunique}) that hands back
11
+ # the codes as storage. {#unique} answers with the vocabulary alone
12
+ # and {#categorize} wraps both in a {CACategorical} Face; a caller
13
+ # who wants the codes themselves — a position to scatter into, a key
14
+ # to group by, a dense renumbering of sparse keys — would otherwise
15
+ # pay a second pass or take the Face and its Ruby label list.
16
+ #
17
+ # `codes` takes the narrowest unsigned data type the vocabulary
18
+ # fits, reserving that type's top value as the exclusion sentinel.
19
+ # A cell that joins no category — a masked cell — is both masked and
20
+ # holds the sentinel, exactly as {CACategorical}'s storage is, so a
21
+ # consumer may read either.
22
+ #
23
+ # Distinctness is the family's hash-key judgement (see {#unique}):
24
+ # `==` for numeric with all NaN collapsed to one value and
25
+ # -0.0 / +0.0 the same value; `eql?` / `hash` for `CA_OBJECT` and
26
+ # `CA_FIXLEN`. Complex values compare by value, with NaN and -0.0
27
+ # treated the same way in each component.
28
+ #
29
+ # There is no `sort:` here, unlike {#unique} and {#value_counts}:
30
+ # the codes index the levels, so reordering the vocabulary would
31
+ # desync them. Take {#unique}`(sort: true)` where the codes are not
32
+ # wanted, or sort afterwards and carry the codes through the same
33
+ # permutation.
34
+ #
35
+ # @return [Array(CArray, CArray)] `[codes, levels]`.
36
+ def factorize
37
+ # One linear pass through the shared value hash (C
38
+ # __factorize_appearance__). It writes the sentinel into the
39
+ # excluded cells but leaves the codes unmasked; masking them is what
40
+ # CACategorical.from_codes does on its way to the Face, and this
41
+ # surface owes the same, since it hands the storage out bare.
42
+ codes, levels = __factorize_appearance__
43
+ excluded = codes.eq(CACategorical::SENTINEL[codes.data_type])
44
+ if excluded.count(true) > 0
45
+ codes.mask = codes.has_mask? ? (codes.mask | excluded) : excluded
46
+ end
47
+ [codes, levels]
48
+ end
49
+
50
+ end
@@ -64,7 +64,8 @@ class CArray
64
64
  flat_addr, outer, rest = gather_nd_flat_addr(indices, "gather_nd")
65
65
  out_shape = outer + rest
66
66
  # flatten + 1-D fancy indexing -> CAMapping view -> materialise via .copy.
67
- result = self.flatten[flat_addr].copy
67
+ # A masked coordinate reads as UNDEF: project carries the address mask.
68
+ result = flat_addr.has_mask? ? flatten.project(flat_addr) : self.flatten[flat_addr].copy
68
69
  out_shape.empty? ? result : result.reshape(*out_shape)
69
70
  end
70
71
 
@@ -91,7 +92,12 @@ class CArray
91
92
  # @raise [IndexError] when a coordinate is out of range on any axis.
92
93
  def put_nd (indices, values)
93
94
  flat_addr, _outer, _rest = gather_nd_flat_addr(indices, "put_nd")
94
- self.flatten[flat_addr] = values
95
+ if flat_addr.has_mask?
96
+ # A masked coordinate writes nothing: scatter_replace! skips it.
97
+ flatten.scatter_replace!(flat_addr, values)
98
+ else
99
+ self.flatten[flat_addr] = values
100
+ end
95
101
  self
96
102
  end
97
103
 
@@ -150,6 +156,11 @@ class CArray
150
156
  flat_idx = (flat_idx_raw + dims) % dims
151
157
 
152
158
  base = (flat_idx * strides).sum(axis: 1).int64.reshape(m) # (M,)
159
+ # sum skips a masked coordinate, which would leave its row pointing at
160
+ # the wrong cell: a row with any masked coordinate has no address.
161
+ if flat_idx_raw.has_mask?
162
+ base[flat_idx_raw.is_masked.any(axis: 1)] = UNDEF
163
+ end
153
164
 
154
165
  flat_addr =
155
166
  if rest.empty?
@@ -11,8 +11,8 @@ class CArray
11
11
  # appends at the end) and a negative value counts from the end;
12
12
  # `bsize[i]` must be non-negative. Inserted cells are filled by
13
13
  # the block's return value, or left at the type's default when
14
- # no block is given. Preserves fixlen bytes and Face identity
15
- # (built on the storage layout, re-wrapped via `face_lift`).
14
+ # no block is given. Preserves fixlen bytes and Face identity; the
15
+ # block returns a value of the array (a Time for a time array).
16
16
  # The `offset` array is not mutated.
17
17
  #
18
18
  # @param offset [Array<Integer>] insertion offset per axis.
@@ -40,21 +40,16 @@ class CArray
40
40
  # offset == dim (append) leaves nothing on the upper side to shift.
41
41
  grids[i][offset[i]..-1].seq!(offset[i]+bsize[i]) if offset[i] < shape[i]
42
42
  end
43
- # Build at the storage layout (preserving bytes for fixlen / Face),
44
- # then re-wrap as the same Face.
45
- face_parent = self.face? ? self : nil
46
- src = self
47
- src = src.parent while src.face?
48
- dt = src.data_type
49
- bytes = (dt == :fixlen) ? src.bytes : nil
50
- out = CArray.new(dt, newdim, bytes: bytes)
43
+ # The original cells are copied as storage, so a read-only Face inserts
44
+ # too; the block's value is written as a value of the array, which a
45
+ # read-only Face refuses.
46
+ out = __blank_like__(newdim)
51
47
  if block_given?
52
- sel = out.true
53
- sel[*grids] = 0
48
+ sel = CArray.boolean(*newdim) { true }
49
+ sel[*grids] = false
54
50
  out[sel] = block.call
55
51
  end
56
- out[*grids] = src
57
- out = out.face_lift(face_parent) if face_parent
52
+ out.__send__(:__face_storage__)[*grids] = __face_storage__
58
53
  return out
59
54
  end
60
55
 
@@ -15,14 +15,29 @@ class CArray
15
15
  # {CArray.result_type}), so membership is value-correct across
16
16
  # data types (e.g. an int cell equals a float set element of the same
17
17
  # value, and a fractional set element never truncates onto an int
18
- # cell). Genuinely incompatible data types (e.g. numeric vs fixlen)
19
- # raise.
18
+ # cell). Integers of mixed sign are matched by value, as {#eq}
19
+ # compares them: `-1` in an int8 array never matches `255` in a uint8
20
+ # one, and the set operations answer such a pair in a type that holds
21
+ # both (int16 for uint8 with int8, object for uint64 with int64).
22
+ # Genuinely incompatible data types (e.g. numeric vs fixlen) raise.
20
23
  #
21
24
  # Membership is value-based and shares the distinctness of the
22
25
  # value-hash discovery family ({#unique} / {#value_counts}):
23
26
  # numeric follows `==` with all NaN collapsed to one value and
24
27
  # -0.0 == +0.0; `CA_OBJECT` follows Ruby `hash` / `eql?` with Float
25
- # NaN collapsed; `CA_FIXLEN` follows byte equality.
28
+ # NaN collapsed; `CA_FIXLEN` follows byte equality over the whole
29
+ # cell, with a short String in the set padded out to the cell width
30
+ # (so a 5-byte array written from `"be"` is `is_in(["be"])`). A
31
+ # `CA_FIXLEN` set given as a CArray must already be of that width.
32
+ #
33
+ # An object array keeps Ruby's own split between its two kinds of
34
+ # equality. A lookup through a hash -- `is_in`, the set operations,
35
+ # {#locate_addr}, {#unique} and the rest of the discovery family --
36
+ # uses `eql?`, as `Array#uniq`, `Array#&` and `Set` do, so `1` and
37
+ # `1.0` are different values there. A cell-by-cell comparison --
38
+ # {#eq}, {#count}, {#search}, {#bsearch} -- uses `==`, as
39
+ # `Array#include?` does, so they are equal there. To match by value
40
+ # across Integer and Float, convert the array to a numeric type first.
26
41
  #
27
42
  # Masked cells of `values` do not enter the set. Masked cells of
28
43
  # `self` stay masked in the result (membership is unknown), so
@@ -61,6 +76,7 @@ class CArray
61
76
  def intersection (other, sort: false)
62
77
  a, b = promote_value_set(other)
63
78
  r = a.__send__(:__intersection__, b)
79
+ r = r.to_unit(unit) if !a.equal?(self) && respond_to?(:in_common_unit_with)
64
80
  sort ? r.sort : r
65
81
  end
66
82
 
@@ -75,13 +91,17 @@ class CArray
75
91
  def difference (other, sort: false)
76
92
  a, b = promote_value_set(other)
77
93
  r = a.__send__(:__difference__, b)
94
+ r = r.to_unit(unit) if !a.equal?(self) && respond_to?(:in_common_unit_with)
78
95
  sort ? r.sort : r
79
96
  end
80
97
 
81
98
  # @overload union(other, sort: false)
82
99
  # Returns a 1-D CArray of the distinct values appearing in either
83
100
  # `self` or `other`, in self-then-other first-appearance order.
84
- # See {#intersection} for the shared semantics and options.
101
+ # See {#intersection} for the shared semantics and options. Two time
102
+ # arrays of different units give their union in the finer unit, which
103
+ # holds the values of both ({#intersection} and {#difference} answer in
104
+ # `self`'s unit, since their values are `self`'s).
85
105
  #
86
106
  # @param other [CArray, Array, Range] promoted with `self`.
87
107
  # @param sort [Boolean] when true, return the values sorted ascending
@@ -110,13 +130,60 @@ class CArray
110
130
  # operand (Numo, a MemoryView producer, ...) comes in through wrap_readonly,
111
131
  # the canonical type-coercion entry, so its format's data type drives the promote.
112
132
  def promote_value_set (values)
133
+ # Two time arrays of different units meet in the finer one.
134
+ if values.is_a?(CArray) && values.class == self.class &&
135
+ respond_to?(:in_common_unit_with)
136
+ return in_common_unit_with(values)
137
+ end
138
+ values = values_for_lookup(values) if values.is_a?(CArray)
139
+ # Two fixed-width string arrays of different widths meet at the wider.
140
+ if values.is_a?(CArray) && data_type == CA_FIXLEN && values.data_type == CA_FIXLEN
141
+ return fixlen_widened_with(values)
142
+ end
143
+ # A Face that reconciles operands (CATime) reads a bare Array as values
144
+ # of its own kind -- instants written as Time / String / elements.
145
+ if values.is_a?(Array) && face? && respond_to?(:to_comparable)
146
+ return [self, to_comparable(values)]
147
+ end
148
+ # A single value (a Time, an element, a bare number) is a set of one,
149
+ # read the same way; a bare number is refused like any other operand
150
+ # that is not of its kind.
151
+ if !values.is_a?(CArray) && !values.is_a?(Range) && face? &&
152
+ respond_to?(:to_comparable)
153
+ return [self, to_comparable([values])]
154
+ end
155
+ # ...and so does an array of values that are not of its kind (an object
156
+ # column of Time values, a string column).
157
+ if values.is_a?(CArray) && respond_to?(:to_comparable) &&
158
+ !values.respond_to?(:to_comparable)
159
+ return [self, values_in_face(self, values) || to_comparable([])]
160
+ end
161
+ # The same from the other side: values of a time array looked for in an
162
+ # array that is not one.
163
+ if values.is_a?(CArray) && values.respond_to?(:to_comparable) &&
164
+ !respond_to?(:to_comparable)
165
+ return [values_in_face(values, self) || values.to_comparable([]), values]
166
+ end
167
+ # A Face whose storage is not of its surface type. When its cells are
168
+ # its values, the set meets it as it would meet an array of the storage
169
+ # type, and the Face stays while that type does not change. Any other
170
+ # Face leaves the set to the lookup, which brings it into the Face's
171
+ # space or refuses it.
172
+ if face? && (storage = __face_storage__).data_type != data_type
173
+ unless __comparable_storage__
174
+ set = values.is_a?(Range) ? values.to_a : values
175
+ return [self, set.is_a?(CArray) ? set : CArray.wrap_readonly(set)]
176
+ end
177
+ s, set = storage.send(:promote_value_set, values)
178
+ return [s.equal?(storage) ? self : s, set]
179
+ end
113
180
  case values
114
181
  when Array then return promote_elements(values)
115
182
  when Range then return promote_elements(values.to_a)
116
183
  end
117
184
  set = values.is_a?(CArray) ? values : CArray.wrap_readonly(values)
118
185
  return [self, set] if set.data_type == data_type # common fast path
119
- t = CArray.result_type(self, set)
186
+ t = value_match_type(CArray.result_type(self, set), set.data_type)
120
187
  [coerce_self(t), set.to_type(t)]
121
188
  end
122
189
 
@@ -126,14 +193,71 @@ class CArray
126
193
  # object / fixlen self the elements are values, not data type specifiers (a
127
194
  # String is a value, not a type name), so build the set in self's data type.
128
195
  def promote_elements (elems)
129
- if data_type == CA_OBJECT || data_type == CA_FIXLEN
196
+ if data_type == CA_FIXLEN
197
+ # A fixlen value is a cell-width blob, so the set has to be built at
198
+ # this array's width -- to_type(:fixlen) with no bytes: builds it at
199
+ # width 0, where every element is "" and the C guard then refuses the
200
+ # set for not matching self. Same rule the scalar operand of a
201
+ # comparison follows: a String standing in for a cell is padded to the
202
+ # cell's width.
203
+ [self, elems.to_ca.to_type(data_type, bytes: bytes)]
204
+ elsif data_type == CA_OBJECT
130
205
  [self, elems.to_ca.to_type(data_type)]
131
206
  else
132
207
  t = CArray.result_type(self, *elems)
208
+ t = value_match_type(t, CA_INT64) if elems.any? { |v| v.is_a?(Integer) && v < 0 }
133
209
  [coerce_self(t), elems.to_ca.to_type(t)]
134
210
  end
135
211
  end
136
212
 
213
+ # +other+ as a lookup against self reads it. A string array whose cells
214
+ # hold ranges into a pool of its own (CAConstString) is read by its
215
+ # strings, and a categorical by its labels, since their storage is not
216
+ # their values; anything else, or the same kind of array as self, is
217
+ # taken as it is. A record is left to be refused.
218
+ def values_for_lookup (other)
219
+ return other if other.send(:__orderable_storage__)
220
+ return other if face? && other.class == self.class
221
+ if other.respond_to?(:to_string)
222
+ other.to_string
223
+ elsif other.is_a?(CACategorical)
224
+ other.to_type(:object)
225
+ else
226
+ other
227
+ end
228
+ end
229
+
230
+ # [self, other] at one cell width when both are fixed-width string arrays
231
+ # (CAFixlenString) of different widths: the narrower is padded with NUL to
232
+ # the wider, as concatenate joins them, so a string matches whatever width
233
+ # it is held at. Raw fixlen cells of different widths are values of
234
+ # different lengths, not padded strings, and are left as they are (the
235
+ # lookup refuses them, naming the widths).
236
+ def fixlen_widened_with (other)
237
+ return [self, other] if bytes == other.bytes
238
+ return [self, other] unless is_a?(CAFixlenString) && other.is_a?(CAFixlenString)
239
+ w = [bytes, other.bytes].max
240
+ [widen_fixlen_string(self, w), widen_fixlen_string(other, w)]
241
+ end
242
+
243
+ def widen_fixlen_string (x, w)
244
+ x.bytes == w ? x : x.to_fixlen_string(bytes: w)
245
+ end
246
+
247
+ # The values of +other+, which has no to_comparable, read as values of +face+'s
248
+ # kind (instants written as Time / Date / String for a time key), in an
249
+ # array of +other+'s shape; a masked cell stays masked. nil when every
250
+ # cell is masked: there is nothing to read.
251
+ private def values_in_face(face, other)
252
+ return face.to_comparable(other) if other.is_a?(Array)
253
+ vals = other.flatten.to_a
254
+ filler = vals.find { |v| !UNDEF.equal?(v) }
255
+ return nil if filler.equal?(nil) && vals.all? { |v| UNDEF.equal?(v) }
256
+ out = face.to_comparable(vals.map { |v| UNDEF.equal?(v) ? filler : v })
257
+ out[other.is_masked.flatten] = UNDEF if other.has_mask?
258
+ out.reshape(*other.shape)
259
+ end
260
+
137
261
  def coerce_self (t)
138
262
  CArray.result_type(self) == t ? self : to_type(t)
139
263
  end