carray 3.0.3 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +1 -6
  7. data/ext/ca_axis_group.c +27 -27
  8. data/ext/ca_binop_dispatch.c +1 -29
  9. data/ext/ca_binop_dispatch.h +0 -1
  10. data/ext/ca_categorical_iterator.c +205 -134
  11. data/ext/ca_compare.c +7 -18
  12. data/ext/ca_group_iter.c +16 -17
  13. data/ext/ca_kernel_iterator.c +1234 -1994
  14. data/ext/ca_kernel_iterator.h +391 -437
  15. data/ext/ca_monop_dispatch.c +82 -59
  16. data/ext/ca_monop_dispatch.h +1 -0
  17. data/ext/ca_obj_array.c +96 -39
  18. data/ext/ca_obj_bincmp.c +24 -21
  19. data/ext/ca_obj_binop.c +15 -26
  20. data/ext/ca_obj_bitarray.c +3 -2
  21. data/ext/ca_obj_bitfield.c +5 -6
  22. data/ext/ca_obj_block.c +18 -9
  23. data/ext/ca_obj_byte_swap.c +5 -6
  24. data/ext/ca_obj_const_string.c +173 -19
  25. data/ext/ca_obj_face.c +238 -57
  26. data/ext/ca_obj_face.h +19 -0
  27. data/ext/ca_obj_fake.c +15 -29
  28. data/ext/ca_obj_farray.c +6 -3
  29. data/ext/ca_obj_field.c +17 -5
  30. data/ext/ca_obj_fixlen_string.c +2 -3
  31. data/ext/ca_obj_grid.c +406 -132
  32. data/ext/ca_obj_meld.c +172 -34
  33. data/ext/ca_obj_moncmp.c +23 -8
  34. data/ext/ca_obj_monop.c +67 -57
  35. data/ext/ca_obj_object.c +157 -51
  36. data/ext/ca_obj_record.c +12 -7
  37. data/ext/ca_obj_reduce.c +13 -8
  38. data/ext/ca_obj_refer.c +25 -27
  39. data/ext/ca_obj_remap.c +30 -9
  40. data/ext/ca_obj_repeat.c +9 -10
  41. data/ext/ca_obj_roll.c +6 -5
  42. data/ext/ca_obj_select.c +14 -9
  43. data/ext/ca_obj_select_axis.c +15 -47
  44. data/ext/ca_obj_shift.c +11 -1
  45. data/ext/ca_obj_stack.c +179 -34
  46. data/ext/ca_obj_stride.c +162 -115
  47. data/ext/ca_obj_string.c +71 -4
  48. data/ext/ca_obj_tile.c +6 -5
  49. data/ext/ca_obj_time.c +22 -6
  50. data/ext/ca_obj_timedelta.c +22 -6
  51. data/ext/ca_obj_transpose.c +5 -3
  52. data/ext/ca_obj_triop.c +23 -32
  53. data/ext/ca_obj_window.c +190 -26
  54. data/ext/ca_sweep_engine.c +2 -2
  55. data/ext/caframe_csv_reader.c +80 -1
  56. data/ext/carray.h +181 -75
  57. data/ext/carray_access.c +184 -96
  58. data/ext/carray_address_basis.c +343 -87
  59. data/ext/carray_attribute.c +89 -6
  60. data/ext/carray_bincount.c +10 -8
  61. data/ext/carray_broadcast.c +13 -8
  62. data/ext/carray_build_flags.h +3 -0
  63. data/ext/carray_call_cfunc.c +108 -0
  64. data/ext/carray_cast.c +142 -24
  65. data/ext/carray_cast_func.rb +46 -6
  66. data/ext/carray_class.c +4 -3
  67. data/ext/carray_config.h +28 -0
  68. data/ext/carray_conversion.c +16 -7
  69. data/ext/carray_copy.c +51 -14
  70. data/ext/carray_core.c +147 -55
  71. data/ext/carray_count.c +50 -20
  72. data/ext/carray_element.c +76 -33
  73. data/ext/carray_factorize.c +118 -64
  74. data/ext/carray_generate.c +191 -50
  75. data/ext/carray_histogram.c +62 -17
  76. data/ext/carray_hold.c +12 -15
  77. data/ext/carray_index_classifier.c +388 -176
  78. data/ext/carray_internal.h +336 -29
  79. data/ext/carray_kernels_bincmp.c +4453 -0
  80. data/ext/carray_kernels_binop.c +11368 -0
  81. data/ext/carray_kernels_init.c +1734 -0
  82. data/ext/carray_kernels_moncmp.c +2104 -0
  83. data/ext/carray_kernels_monop.c +18369 -0
  84. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  85. data/ext/carray_kernels_reduce_boolean.c +406 -0
  86. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  87. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  88. data/ext/carray_kernels_reduce_variance.c +3849 -0
  89. data/ext/carray_kernels_scan.c +4302 -0
  90. data/ext/carray_kernels_search.c +33829 -0
  91. data/ext/carray_kernels_sort.c +10906 -0
  92. data/ext/carray_kernels_triop.c +1864 -0
  93. data/ext/carray_key_windows.c +216 -0
  94. data/ext/carray_lazy.c +170 -111
  95. data/ext/carray_mask.c +292 -72
  96. data/ext/carray_median_percentile.c +82 -28
  97. data/ext/carray_memory_view.c +669 -561
  98. data/ext/carray_operator.c +124 -47
  99. data/ext/carray_order.c +384 -76
  100. data/ext/carray_partition.c +28 -27
  101. data/ext/carray_random.c +67 -13
  102. data/ext/carray_round.c +273 -0
  103. data/ext/carray_scatter.c +63 -38
  104. data/ext/carray_segment.c +3 -3
  105. data/ext/carray_slab.c +91 -28
  106. data/ext/carray_sort.c +35 -35
  107. data/ext/carray_struct.c +19 -18
  108. data/ext/carray_test.c +26 -5
  109. data/ext/carray_undef.c +4 -0
  110. data/ext/carray_utils.c +246 -45
  111. data/ext/mk_call_cfunc.rb +19 -2
  112. data/ext/mkkernel.rb +198 -240
  113. data/ext/ruby_carray.c +10 -4
  114. data/ext/version.h +4 -4
  115. data/lib/carray/attribute.rb +126 -22
  116. data/lib/carray/autoload_carray.rb +8 -31
  117. data/lib/carray/autoload_method_extension.rb +14 -5
  118. data/lib/carray/axis_group.rb +255 -106
  119. data/lib/carray/basics.rb +78 -10
  120. data/lib/carray/bincount_nd.rb +12 -2
  121. data/lib/carray/block_iterator.rb +126 -23
  122. data/lib/carray/boolean_reduce.rb +17 -17
  123. data/lib/carray/categorical.rb +165 -43
  124. data/lib/carray/categorical_iterator.rb +130 -27
  125. data/lib/carray/complex.rb +5 -5
  126. data/lib/carray/conditional.rb +67 -18
  127. data/lib/carray/const_string.rb +14 -5
  128. data/lib/carray/construct.rb +15 -11
  129. data/lib/carray/data_type_extension.rb +1 -0
  130. data/lib/carray/distance_limit.rb +77 -0
  131. data/lib/carray/fixlen_string.rb +7 -1
  132. data/lib/carray/frame/column_verbs.rb +70 -0
  133. data/lib/carray/frame/convert.rb +69 -0
  134. data/lib/carray/frame/csv_parser.rb +111 -17
  135. data/lib/carray/frame/describe.rb +3 -2
  136. data/lib/carray/frame/frame.rb +246 -21
  137. data/lib/carray/frame/group.rb +510 -95
  138. data/lib/carray/frame/io.rb +115 -28
  139. data/lib/carray/frame/join.rb +312 -51
  140. data/lib/carray/frame/records.rb +83 -7
  141. data/lib/carray/frame/reshape.rb +42 -35
  142. data/lib/carray/frame/sort.rb +7 -19
  143. data/lib/carray/frame/verbs.rb +188 -48
  144. data/lib/carray/frame/windows.rb +117 -0
  145. data/lib/carray/frame.rb +2 -0
  146. data/lib/carray/fuse_source.rb +11 -4
  147. data/lib/carray/fusion.rb +40 -16
  148. data/lib/carray/histogram.rb +6 -1
  149. data/lib/carray/inspect.rb +31 -8
  150. data/lib/carray/iterator.rb +85 -7
  151. data/lib/carray/key_windows.rb +620 -0
  152. data/lib/carray/lazy.rb +182 -183
  153. data/lib/carray/mask_gap_fill.rb +63 -57
  154. data/lib/carray/meld_reduce.rb +12 -14
  155. data/lib/carray/methods/bin.rb +26 -18
  156. data/lib/carray/methods/bincount.rb +16 -0
  157. data/lib/carray/methods/bit_string.rb +1 -1
  158. data/lib/carray/methods/choose.rb +16 -3
  159. data/lib/carray/methods/composition.rb +3 -1
  160. data/lib/carray/methods/gather_nd.rb +14 -5
  161. data/lib/carray/methods/insert_block.rb +2 -2
  162. data/lib/carray/methods/is_between.rb +59 -0
  163. data/lib/carray/methods/is_in.rb +53 -36
  164. data/lib/carray/methods/join.rb +1 -6
  165. data/lib/carray/methods/lag.rb +49 -0
  166. data/lib/carray/methods/locate_addr.rb +47 -51
  167. data/lib/carray/methods/mode.rb +1 -1
  168. data/lib/carray/methods/nunique.rb +11 -6
  169. data/lib/carray/methods/resize.rb +3 -3
  170. data/lib/carray/methods/value_counts.rb +4 -11
  171. data/lib/carray/record_order.rb +3 -3
  172. data/lib/carray/rng.rb +3 -0
  173. data/lib/carray/runtime.rb +53 -8
  174. data/lib/carray/segment_iterator.rb +313 -82
  175. data/lib/carray/serialize.rb +24 -10
  176. data/lib/carray/slab_iterator.rb +50 -0
  177. data/lib/carray/string.rb +105 -5
  178. data/lib/carray/string_operation_extension.rb +8 -1
  179. data/lib/carray/struct.rb +9 -21
  180. data/lib/carray/struct_builder.rb +19 -8
  181. data/lib/carray/time.rb +726 -104
  182. data/lib/carray/window_iterator.rb +122 -16
  183. data/lib/carray.rb +7 -0
  184. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  185. data/yard-stubs/ca_obj_field.rb +4 -0
  186. data/yard-stubs/ca_obj_grid.rb +21 -10
  187. data/yard-stubs/ca_obj_refer.rb +2 -1
  188. data/yard-stubs/ca_obj_roll.rb +2 -2
  189. data/yard-stubs/ca_obj_shift.rb +5 -1
  190. data/yard-stubs/ca_obj_tile.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +8 -3
  192. data/yard-stubs/carray_access.rb +6 -1
  193. data/yard-stubs/carray_attribute.rb +0 -5
  194. data/yard-stubs/carray_cast.rb +10 -0
  195. data/yard-stubs/carray_conversion.rb +2 -1
  196. data/yard-stubs/carray_copy.rb +4 -2
  197. data/yard-stubs/carray_count.rb +4 -4
  198. data/yard-stubs/carray_element.rb +10 -4
  199. data/yard-stubs/carray_generate.rb +21 -1
  200. data/yard-stubs/carray_mask.rb +26 -2
  201. data/yard-stubs/carray_math.rb +27 -1
  202. data/yard-stubs/carray_median_percentile.rb +5 -2
  203. data/yard-stubs/carray_memory_view.rb +40 -23
  204. data/yard-stubs/carray_order.rb +29 -18
  205. data/yard-stubs/carray_random.rb +13 -4
  206. data/yard-stubs/carray_scatter.rb +14 -9
  207. data/yard-stubs/carray_slab.rb +9 -2
  208. data/yard-stubs/carray_sort.rb +6 -5
  209. data/yard-stubs/carray_test.rb +5 -3
  210. metadata +25 -3
  211. data/lib/carray/arrow_tensor.rb +0 -401
  212. data/lib/carray/table.rb +0 -108
@@ -15,7 +15,7 @@
15
15
  # `strip_mask(:na)`). Passing both raises ArgumentError.
16
16
  #
17
17
  # Design / rationale: devel/PROPOSAL_UNMASK_FILL_METHOD.md. The forward
18
- # hold is the C primitive #__hold__; backward = flip -> forward -> flip,
18
+ # hold is the C primitive #hold_forward; backward = flip -> forward -> flip,
19
19
  # flatten = flatten -> hold -> reshape, and :linear composes
20
20
  # linear_section / linear_fetch. User-facing YARD docs live in
21
21
  # yard-stubs/carray_mask.rb.
@@ -29,50 +29,50 @@
29
29
 
30
30
  class CArray
31
31
 
32
- # Preserve the C constant-fill primitives under private names so the
33
- # Ruby wrappers can delegate the non-method path unchanged.
34
- alias __unmask_const__ unmask
35
- alias __strip_mask_const__ strip_mask
36
- private :__unmask_const__, :__strip_mask_const__
37
-
38
32
  # Sentinel for "no positional fill value given" (distinct from any real
39
33
  # fill value, including nil / false / a Symbol).
40
34
  MASK_FILL_UNSET = ::Object.new
35
+ Ractor.make_shareable(MASK_FILL_UNSET)
41
36
  private_constant :MASK_FILL_UNSET
42
37
 
43
- def unmask (fill = MASK_FILL_UNSET, method: nil, axis: nil)
44
- if method
45
- unless fill.equal?(MASK_FILL_UNSET)
46
- raise ArgumentError,
47
- "unmask: pass either a constant fill value or method:, not both"
48
- end
49
- held = __gap_fill__(method, axis, "unmask")
50
- # Copy the filled values in place (a Face takes a Face of its own kind
51
- # as it is).
52
- value[] = held.value
53
- if held.has_mask?
54
- self.mask = held.mask # residual leading/trailing mask
55
- else
56
- __unmask_const__ # fully filled: drop the mask
38
+ # unmask / strip_mask with method:, in front of the C constant-fill
39
+ # primitives, which they reach with super.
40
+ module GapFill
41
+ def unmask (fill = MASK_FILL_UNSET, method: nil, axis: nil)
42
+ if method
43
+ unless fill.equal?(MASK_FILL_UNSET)
44
+ raise ArgumentError,
45
+ "unmask: pass either a constant fill value or method:, not both"
46
+ end
47
+ held = gap_fill(method, axis, "unmask")
48
+ # Copy the filled values in place (a Face takes a Face of its own kind
49
+ # as it is).
50
+ value[] = held.value
51
+ if held.has_mask?
52
+ self.mask = held.mask # residual leading/trailing mask
53
+ else
54
+ super() # fully filled: drop the mask
55
+ end
56
+ return self
57
57
  end
58
- return self
58
+ fill.equal?(MASK_FILL_UNSET) ? super() : super(fill)
59
59
  end
60
- fill.equal?(MASK_FILL_UNSET) ? __unmask_const__ : __unmask_const__(fill)
61
- end
62
60
 
63
- def strip_mask (fill = MASK_FILL_UNSET, method: nil, axis: nil)
64
- if method
65
- unless fill.equal?(MASK_FILL_UNSET)
66
- raise ArgumentError,
67
- "strip_mask: pass either a constant fill value or method:, not both"
61
+ def strip_mask (fill = MASK_FILL_UNSET, method: nil, axis: nil)
62
+ if method
63
+ unless fill.equal?(MASK_FILL_UNSET)
64
+ raise ArgumentError,
65
+ "strip_mask: pass either a constant fill value or method:, not both"
66
+ end
67
+ return carry_attrs(gap_fill(method, axis, "strip_mask"))
68
68
  end
69
- return __gap_fill__(method, axis, "strip_mask")
70
- end
71
- if fill.equal?(MASK_FILL_UNSET)
72
- raise ArgumentError, "strip_mask: a fill value is required (or method:)"
69
+ if fill.equal?(MASK_FILL_UNSET)
70
+ raise ArgumentError, "strip_mask: a fill value is required (or method:)"
71
+ end
72
+ carry_attrs(super(fill))
73
73
  end
74
- __strip_mask_const__(fill)
75
74
  end
75
+ prepend GapFill
76
76
 
77
77
  private
78
78
 
@@ -80,15 +80,15 @@ class CArray
80
80
  # unfillable cells stay masked; a fully filled result carries no mask
81
81
  # (hold allocates the output mask only when a leading run goes UNDEF;
82
82
  # :linear masks only the exterior via mask_invalid).
83
- def __gap_fill__ (method, axis, name)
83
+ def gap_fill (method, axis, name)
84
84
  axis = normalize_axis(axis, name) unless axis.nil?
85
85
  case method
86
86
  when :forward, :ffill
87
- __hold_axis__(axis, false)
87
+ hold_along_axis(axis, false)
88
88
  when :backward, :bfill
89
- __hold_axis__(axis, true)
89
+ hold_along_axis(axis, true)
90
90
  when :linear
91
- __gap_fill_linear__(axis)
91
+ gap_fill_linear(axis)
92
92
  else
93
93
  raise ArgumentError,
94
94
  "unmask/strip_mask: unknown method: #{method.inspect} " \
@@ -97,20 +97,20 @@ class CArray
97
97
  end
98
98
 
99
99
  # Forward (backward = false) or backward (true) hold along `axis`, already
100
- # normalised by __gap_fill__ (axis nil = flatten). Backward reuses the forward primitive on the
100
+ # normalised by gap_fill (axis nil = flatten). Backward reuses the forward primitive on the
101
101
  # reversed view; flatten flattens, holds axis 0, reshapes back.
102
- def __hold_axis__ (axis, backward)
102
+ def hold_along_axis (axis, backward)
103
103
  if axis.nil?
104
104
  flat = flatten
105
105
  flat = flat.reverse if backward
106
- held = flat.send(:__hold__, 0)
106
+ held = flat.send(:hold_forward, 0)
107
107
  held = held.reverse if backward
108
108
  held.reshape(*shape)
109
109
  else
110
110
  if backward
111
- flip(axis).send(:__hold__, axis).flip(axis)
111
+ flip(axis).send(:hold_forward, axis).flip(axis)
112
112
  else
113
- send(:__hold__, axis)
113
+ send(:hold_forward, axis)
114
114
  end
115
115
  end
116
116
  end
@@ -121,9 +121,9 @@ class CArray
121
121
  # linear_fetch, which keeps its unit and rounds to its grid. Defining one
122
122
  # is how a Face says it interpolates: without it, the core linear_fetch
123
123
  # reads the Face's storage and fails from inside.
124
- def __gap_fill_linear__ (axis)
124
+ def gap_fill_linear (axis)
125
125
  if face?
126
- unless __interpolable__
126
+ unless interpolable?
127
127
  raise ArgumentError,
128
128
  "unmask/strip_mask(method: :linear): #{self.class} does not " \
129
129
  "interpolate (it defines no linear_fetch of its own); " \
@@ -135,15 +135,15 @@ class CArray
135
135
  "required (got #{data_type_name})"
136
136
  end
137
137
  if axis.nil?
138
- return __linear_fiber__(flatten).reshape(*shape)
138
+ return linear_fill_fiber(flatten).reshape(*shape)
139
139
  end
140
140
  ax = axis
141
141
  # A fiber comes back in self's own type (a Face's linear_fetch has already
142
142
  # rounded to its grid), so the result assembles in that type and the
143
143
  # present cells are never carried through float64.
144
144
  out = face? ? template : value.copy
145
- __each_fiber_key__(ax) do |key|
146
- out[*key] = __linear_fiber__(self[*key])
145
+ each_fiber_key(ax) do |key|
146
+ out[*key] = linear_fill_fiber(self[*key])
147
147
  end
148
148
  out
149
149
  end
@@ -151,13 +151,13 @@ class CArray
151
151
  # Whether linear interpolation can fill this array: a numeric array, or a
152
152
  # Face that defines a linear_fetch of its own -- which is how a Face says it
153
153
  # interpolates (the core linear_fetch would read its storage).
154
- private def __interpolable__
155
- face? ? self.class.instance_method(:linear_fetch).owner != CArray : numeric?
154
+ private def interpolable?
155
+ face? ? self.class.instance_method(:linear_fetch).owner != CArray : numeric? # type-label: asks what the Face can do, names nothing
156
156
  end
157
157
 
158
158
  # 1-D linear-by-index fill of a single (possibly masked) fiber.
159
- def __linear_fiber__ (vec)
160
- vec.send(:__linear_fill_along__, CArray.float64(vec.elements) { |i| i.to_f })
159
+ def linear_fill_fiber (vec)
160
+ vec.send(:linear_fill_along, CArray.float64(vec.elements) { |i| i.to_f })
161
161
  end
162
162
 
163
163
  # Fill the masked cells of this 1-D array by linear interpolation against
@@ -169,7 +169,7 @@ class CArray
169
169
  # linear_section answers NaN -- a property of x, not of the values, so a
170
170
  # NaN or Inf stored in a present cell is left alone. A Face goes through
171
171
  # its own linear_fetch, which keeps its unit and rounds to its grid.
172
- def __linear_fill_along__ (x)
172
+ def linear_fill_along (x)
173
173
  present = is_not_masked
174
174
  # Fewer than two valid points -> nothing to interpolate between; leave
175
175
  # every masked cell masked.
@@ -180,7 +180,13 @@ class CArray
180
180
  # lands on its own grid.
181
181
  return self[present].linear_fetch(addr)
182
182
  end
183
- interp = value.float64[present].linear_fetch(addr)
183
+ v = value[present]
184
+ # A complex value interpolates its real and imaginary parts.
185
+ interp = v.complex? ? v.linear_fetch(addr) : v.float64.linear_fetch(addr)
186
+ # An integer array takes the nearest integer, half away from zero (as a
187
+ # time Face lands on its own grid), rounded before the store would cut
188
+ # the fraction off.
189
+ interp = interp.round if integer?
184
190
  # Built from the values alone, so a fully filled result carries no mask.
185
191
  out = value.copy
186
192
  masked = is_masked
@@ -194,7 +200,7 @@ class CArray
194
200
  # Yield an index key (Array with `nil` at `axis`, integers elsewhere)
195
201
  # for every fiber along `axis`. `self[*key]` is that fiber as a masked
196
202
  # 1-D view.
197
- def __each_fiber_key__ (axis)
203
+ def each_fiber_key (axis)
198
204
  outer = shape
199
205
  outer_dims = outer.each_index.reject { |k| k == axis }.map { |k| outer[k] }
200
206
  idx = Array.new(outer_dims.size, 0)
@@ -210,7 +216,7 @@ class CArray
210
216
  end
211
217
  end
212
218
  end
213
- private :__hold_axis__, :__gap_fill_linear__, :__linear_fiber__,
214
- :__each_fiber_key__
219
+ private :hold_along_axis, :gap_fill_linear, :linear_fill_fiber,
220
+ :each_fiber_key
215
221
 
216
222
  end
@@ -16,7 +16,7 @@
16
16
  #
17
17
  # Fast path activates when:
18
18
  # - axis kwarg is a single Integer (or absent for flat reduce)
19
- # - no mask on CAMeld or any parent (skipna needs per-cell dispatch;
19
+ # - no mask on CAMeld or any parent (skipping masked cells needs per-cell dispatch;
20
20
  # the decompose would not see correct mask propagation across parents)
21
21
  # - no non-:axis kwargs (min_count / fill_value / keep_axis / etc. punt
22
22
  # to super; those change finalisation semantics)
@@ -66,9 +66,9 @@ class CAMeld
66
66
  total_count = parents.map(&:elements).inject(:+)
67
67
  total_sum / total_count.to_f
68
68
  elsif meld_axis_normalized?(axis)
69
- return super if parents.all? { |p| p.dim[meld_axis] == 0 }
69
+ return super if parents.all? { |p| p.shape[meld_axis] == 0 }
70
70
  total_sum = parents.map { |p| p.sum(axis: axis) }.inject(:+)
71
- total_count = parents.map { |p| p.dim[meld_axis] }.inject(:+)
71
+ total_count = parents.map { |p| p.shape[meld_axis] }.inject(:+)
72
72
  total_sum / total_count.to_f
73
73
  else
74
74
  non_meld_axis_decompose(:mean, axis)
@@ -88,8 +88,8 @@ class CAMeld
88
88
  return super if parents.all? { |p| p.elements == 0 }
89
89
  meld_combine_flat(:min)
90
90
  elsif meld_axis_normalized?(axis)
91
- return super if parents.all? { |p| p.dim[meld_axis] == 0 }
92
- nonempty = parents.reject { |p| p.dim[meld_axis] == 0 }
91
+ return super if parents.all? { |p| p.shape[meld_axis] == 0 }
92
+ nonempty = parents.reject { |p| p.shape[meld_axis] == 0 }
93
93
  CArray.stack(nonempty.map { |p| p.min(axis: axis) }).min(axis: 0)
94
94
  else
95
95
  non_meld_axis_decompose(:min, axis)
@@ -107,8 +107,8 @@ class CAMeld
107
107
  return super if parents.all? { |p| p.elements == 0 }
108
108
  meld_combine_flat(:max)
109
109
  elsif meld_axis_normalized?(axis)
110
- return super if parents.all? { |p| p.dim[meld_axis] == 0 }
111
- nonempty = parents.reject { |p| p.dim[meld_axis] == 0 }
110
+ return super if parents.all? { |p| p.shape[meld_axis] == 0 }
111
+ nonempty = parents.reject { |p| p.shape[meld_axis] == 0 }
112
112
  CArray.stack(nonempty.map { |p| p.max(axis: axis) }).max(axis: 0)
113
113
  else
114
114
  non_meld_axis_decompose(:max, axis)
@@ -163,7 +163,7 @@ class CAMeld
163
163
  private
164
164
 
165
165
  def meld_axis_normalized?(axis)
166
- (axis < 0 ? axis + ndim : axis) == meld_axis
166
+ normalize_axis(axis) == meld_axis
167
167
  end
168
168
 
169
169
  # Fast path fires when: no mask anywhere, no non-:axis kwargs, and axis
@@ -177,9 +177,7 @@ class CAMeld
177
177
  return false unless (kw.keys - [:axis]).empty?
178
178
  axis = kw[:axis]
179
179
  unless axis.nil?
180
- return false unless axis.is_a?(Integer)
181
- norm = axis < 0 ? axis + ndim : axis
182
- return false unless norm >= 0 && norm < ndim
180
+ return false unless axis.is_a?(Integer) && axis >= -ndim && axis < ndim # axis-check: a gate only; super normalizes
183
181
  end
184
182
  return false if has_mask?
185
183
  parents.each { |p| return false if p.has_mask? }
@@ -218,7 +216,7 @@ class CAMeld
218
216
  # meld_axis branch and never reaches here.)
219
217
  def non_meld_axis_decompose(op, axis)
220
218
  parts = parents.map { |p| p.public_send(op, axis: axis) }
221
- axis_norm = axis < 0 ? axis + ndim : axis
219
+ axis_norm = normalize_axis(axis)
222
220
  new_meld_axis = axis_norm < meld_axis ? meld_axis - 1 : meld_axis
223
221
  CArray.meld(parts, axis: new_meld_axis).copy
224
222
  end
@@ -241,7 +239,7 @@ class CAMeld
241
239
  return yield if parents.any? { |p| p.elements < min_n }
242
240
  finalize_scalar(welford_flat, sample, sqrt)
243
241
  elsif meld_axis_normalized?(axis)
244
- return yield if parents.any? { |p| p.dim[meld_axis] < min_n }
242
+ return yield if parents.any? { |p| p.shape[meld_axis] < min_n }
245
243
  finalize_vector(welford_axis(axis), sample, sqrt)
246
244
  else
247
245
  # Non-meld-axis: each parent's variance/stddev is independent along
@@ -288,7 +286,7 @@ class CAMeld
288
286
  mean = nil
289
287
  m2 = nil
290
288
  parents.each do |p|
291
- np = p.dim[meld_axis]
289
+ np = p.shape[meld_axis]
292
290
  mp = p.mean(axis: axis)
293
291
  # variance is undefined at n=1; for variancep with n=1, m2_p = 0 by
294
292
  # definition (variance_family already gated n >= min_n).
@@ -18,6 +18,11 @@ class CArray
18
18
  # that side. NaN / masked input cells are always masked in the
19
19
  # output, independently of `lfill` / `ufill`.
20
20
  #
21
+ # When `vmin == vmax` the range is one point: a value equal to it is
22
+ # in bin 0 whatever `bins:` or `step` says, and a value below or above
23
+ # it is out of range. An array whose values are all the same is
24
+ # binned this way by `bin(a.min, a.max, ...)`.
25
+ #
21
26
  # Use `snap(step, offset:)` when the desired output is the snapped
22
27
  # value; use `bin_to(edges)` when the edges are non-uniform.
23
28
  #
@@ -46,23 +51,21 @@ class CArray
46
51
  end
47
52
  raise ArgumentError, "bin: vmin > vmax" if vmin > vmax
48
53
 
49
- n = bins || ((vmax - vmin).to_f / step).round
50
- raise ArgumentError, "bin: n must be >= 1" if n < 1
51
-
54
+ unless bins.nil? || bins.is_a?(Integer)
55
+ raise TypeError, "bin: bins: must be an Integer (got #{bins.inspect})"
56
+ end
52
57
  if vmin == vmax
53
- # Degenerate: zero interval → all cells fall on the single edge;
54
- # with include_max: true they land in bin 0.
55
- out = CArray.int64(*shape) { 0 }
56
- out.mask = self.mask.to_ca if self.has_mask?
57
- if self.float?
58
- inv = self.is_invalid
59
- if inv.count(true) > 0
60
- out.mask = out.has_mask? ? (out.mask | inv) : inv
61
- end
62
- end
63
- return out
58
+ # A range of one point: a value equal to it is in bin 0 whatever the
59
+ # count of bins (or the step), a value below or above it is out of
60
+ # range.
61
+ raise ArgumentError, "bin: n must be >= 1" if bins && bins < 1
62
+ return bin_to(CArray.float64(2) { vmin.to_f },
63
+ lfill: lfill, ufill: ufill, include_max: true)
64
64
  end
65
65
 
66
+ n = bins || ((vmax - vmin).to_f / step).round
67
+ raise ArgumentError, "bin: n must be >= 1" if n < 1
68
+
66
69
  # Delegate to `bin_to` with generated uniform edges — same kernel
67
70
  # (`histbin_ki`) as `histogram`, so semantics are identical.
68
71
  edges = CArray.float64(n + 1).span(vmin..vmax)
@@ -99,8 +102,9 @@ class CArray
99
102
  # @return [CArray] `CA_INT64` array with the same shape as `self`
100
103
  # holding bin indices in `[0, N-1]` (or the fill values / mask
101
104
  # for out-of-range and masked cells).
102
- # @raise [ArgumentError] when `edges` is not 1-D or has fewer
103
- # than 2 values.
105
+ # @raise [ArgumentError] when `edges` is not 1-D, has fewer than 2
106
+ # values, has a masked value or NaN, or decreases anywhere. Equal
107
+ # edges (a bin of zero width) and infinite ones are accepted.
104
108
  # @example
105
109
  # e = CA_FLOAT64([0, 1, 10, 100])
106
110
  # v = CA_FLOAT64([0.5, 5.0, 50.0, -1.0, 200.0])
@@ -120,8 +124,12 @@ class CArray
120
124
  # under -> -1, in-range -> 0..N-1, over -> N.
121
125
  out = src.send(:histbin_ki, e, include_max) - 1
122
126
 
123
- out[:eq, -1] = lfill.nil? ? UNDEF : lfill # under
124
- out[:eq, n] = ufill.nil? ? UNDEF : ufill # over
127
+ # Both sides are found before either is written, so a fill that equals
128
+ # the other side's index (lfill: n) is not taken for it.
129
+ under = out.eq(-1)
130
+ over = out.eq(n)
131
+ out[under] = lfill.nil? ? UNDEF : lfill
132
+ out[over] = ufill.nil? ? UNDEF : ufill
125
133
  out
126
134
  end
127
135
 
@@ -35,6 +35,22 @@ class CArray
35
35
  raise CArray::DataTypeError,
36
36
  "bincount requires an integer label array (got #{data_type_name})"
37
37
  end
38
+ unless length.is_a?(Integer)
39
+ raise TypeError, "bincount: length: must be an Integer (got #{length.inspect})"
40
+ end
41
+ if length < 0
42
+ raise ArgumentError, "bincount: length: must not be negative (got #{length})"
43
+ end
44
+ unless weights.nil?
45
+ unless weights.is_a?(CArray)
46
+ raise ArgumentError, "bincount: weights must be a CArray"
47
+ end
48
+ unless weights.integer? || weights.float?
49
+ raise CArray::DataTypeError,
50
+ "bincount: weights must be numeric (got " \
51
+ "#{CArray::Inspector.type_label(weights)})"
52
+ end
53
+ end
38
54
 
39
55
  if elements.zero?
40
56
  if weights
@@ -69,7 +69,7 @@ class CArray
69
69
  raise ArgumentError,
70
70
  "pack_bits: expected CA_BOOLEAN / CA_UINT8 / CA_INT8 (got #{data_type_name})"
71
71
  end
72
- raise ArgumentError, "pack_bits: 1-D CArray expected (got rank #{rank})" unless rank == 1
72
+ raise ArgumentError, "pack_bits: 1-D CArray expected (got ndim #{ndim})" unless ndim == 1
73
73
  n = elements
74
74
  n_bytes = (n + 7) / 8
75
75
  packed = CArray.uint8(n_bytes) { 0 }
@@ -6,7 +6,8 @@ class CArray
6
6
  #
7
7
  # Where `self == i`, the result takes `choices[i]` -- a scalar
8
8
  # fills those cells, a CArray contributes its corresponding
9
- # cells. The result has the same shape as `self`.
9
+ # cells. The result has the same shape as `self`. Where a label
10
+ # is masked, the result is masked.
10
11
  #
11
12
  # @param choices [Array<CArray, Object>] values indexed by the
12
13
  # labels in `self`; each entry is either a same-shape CArray
@@ -15,7 +16,10 @@ class CArray
15
16
  # When `nil` it is inferred: `CArray.result_type` of the CArray
16
17
  # choices, or `CA_OBJECT` when every choice is a scalar.
17
18
  # @return [CArray] new CArray with the shape of `self` holding
18
- # the chosen values.
19
+ # the chosen values; a {CAString} when every choice is a String
20
+ # or a {CAString} and `data_type` is not given.
21
+ # @raise [IndexError] if a label that is not masked is negative or
22
+ # not less than the number of choices.
19
23
  # @example
20
24
  # ref = CA_INT([[0, 1, 2], [1, 2, 0], [2, 0, 1]])
21
25
  # a = CArray.int(3, 3).seq(1)
@@ -24,6 +28,14 @@ class CArray
24
28
  # ref.choose([a, b, c]) # per-cell pick from a / b / c
25
29
  # ref.choose(["a", "b", "c"]) # recode labels to values
26
30
  def choose (choices, data_type: nil)
31
+ outside = (self.lt(0) | self.ge(choices.size)).where
32
+ if outside.elements > 0
33
+ addr = outside[0]
34
+ raise IndexError,
35
+ "choose: label #{self[addr]} at address #{addr} is out of range " \
36
+ "for #{choices.size} choices"
37
+ end
38
+ text = !data_type && CArray.send(:selection_text?, choices)
27
39
  unless data_type
28
40
  ca = choices.select { |v| v.is_a?(CArray) }
29
41
  data_type = ca.empty? ? CA_OBJECT : CArray.result_type(*ca)
@@ -33,7 +45,8 @@ class CArray
33
45
  s = self.eq(i)
34
46
  out[s] = v.is_a?(CArray) ? v[s] : v
35
47
  end
36
- out
48
+ out[self.is_masked] = UNDEF if has_mask?
49
+ text ? CAString.wrap(out) : out
37
50
  end
38
51
 
39
52
  end
@@ -139,6 +139,7 @@ class CArray
139
139
  # per-piece dim sizes summed along each tile axis (= block-matrix
140
140
  # consistency required across rows / columns).
141
141
  def self.__ragged_paste (list, tdim, axis, data_type, name)
142
+ shared_attrs = attrs_shared_by(list)
142
143
  list = CArray.promote_list(list, data_type: data_type)
143
144
  # promote_list has already enforced homogeneity (= common data_type, and
144
145
  # for Face elements: same Face class + portable + state-compatible). So,
@@ -150,7 +151,7 @@ class CArray
150
151
  # its cell width, and the result is as wide as its widest piece.
151
152
  face_parent = list[0].face? ? list.max_by(&:bytes) : nil
152
153
  if face_parent
153
- list = list.map { |x| x.__send__(:__face_storage__) }
154
+ list = list.map { |x| x.__send__(:face_storage) }
154
155
  end
155
156
  data_type = list[0].data_type
156
157
  # Fixlen pieces of different widths join at the widest, so no cell is
@@ -223,6 +224,7 @@ class CArray
223
224
  obj.paste(idx, item)
224
225
  end
225
226
  obj = obj.face_lift(face_parent) if face_parent
227
+ obj.set_attrs(shared_attrs) unless shared_attrs.empty?
226
228
  obj
227
229
  end
228
230
  private_class_method :__ragged_paste
@@ -5,9 +5,9 @@
5
5
  # Treats the trailing K axes of `indices.shape = (..., K)` as coordinate tuples
6
6
  # and consumes the first K axes of params while carrying the remaining axes (= rest).
7
7
  #
8
- # Implementation goes through a CAMapping view of the flattened array:
8
+ # Implementation goes through an index view (CAGrid) of the flattened array:
9
9
  # `params.flatten[flat_addr]`. Pure Ruby, no additional framework, composed
10
- # entirely from existing indexers as in `take_along_axis`. Since CAMapping is
10
+ # entirely from existing indexers as in `take_along_axis`. Since the view is
11
11
  # writable, write-through via `put_nd` uses last-write-wins semantics (= last
12
12
  # value wins on duplicate indices, same contract as `put_along_axis`). For
13
13
  # accumulate semantics, route to the `scatter_*!` family instead.
@@ -63,7 +63,7 @@ class CArray
63
63
  def gather_nd (indices)
64
64
  flat_addr, outer, rest = gather_nd_flat_addr(indices, "gather_nd")
65
65
  out_shape = outer + rest
66
- # flatten + 1-D fancy indexing -> CAMapping view -> materialise via .copy.
66
+ # flatten + 1-D fancy indexing -> CAGrid view -> materialise via .copy.
67
67
  # A masked coordinate reads as UNDEF: project carries the address mask.
68
68
  result = flat_addr.has_mask? ? flatten.project(flat_addr) : self.flatten[flat_addr].copy
69
69
  out_shape.empty? ? result : result.reshape(*out_shape)
@@ -91,10 +91,19 @@ class CArray
91
91
  # @raise [ArgumentError] on the same conditions as {#gather_nd}.
92
92
  # @raise [IndexError] when a coordinate is out of range on any axis.
93
93
  def put_nd (indices, values)
94
+ # The write goes through a flattened view, which carries a frozen
95
+ # array's read-only flag but not its frozenness; say what the array is.
96
+ raise FrozenError.new("can't modify frozen #{self.class}", receiver: self) if frozen?
94
97
  flat_addr, _outer, _rest = gather_nd_flat_addr(indices, "put_nd")
95
98
  if flat_addr.has_mask?
96
- # A masked coordinate writes nothing: scatter_replace! skips it.
97
- flatten.scatter_replace!(flat_addr, values)
99
+ # A masked coordinate writes nothing. The values are laid out over
100
+ # the coordinates by the store itself, so they mean what they mean
101
+ # unmasked, and only the cells at unmasked coordinates are written.
102
+ flat = flatten
103
+ vals = flat.project(flat_addr)
104
+ vals[] = values
105
+ sel = flat_addr.is_not_masked
106
+ flat[flat_addr[sel]] = vals[sel]
98
107
  else
99
108
  self.flatten[flat_addr] = values
100
109
  end
@@ -43,13 +43,13 @@ class CArray
43
43
  # The original cells are copied as storage, so a read-only Face inserts
44
44
  # too; the block's value is written as a value of the array, which a
45
45
  # read-only Face refuses.
46
- out = __blank_like__(newdim)
46
+ out = blank_like(newdim)
47
47
  if block_given?
48
48
  sel = CArray.boolean(*newdim) { true }
49
49
  sel[*grids] = false
50
50
  out[sel] = block.call
51
51
  end
52
- out.__send__(:__face_storage__)[*grids] = __face_storage__
52
+ out.__send__(:face_storage)[*grids] = face_storage
53
53
  return out
54
54
  end
55
55
 
@@ -0,0 +1,59 @@
1
+ class CArray
2
+
3
+ # @!group Elementwise math
4
+
5
+ # @overload is_between(lo, hi, exclude: nil)
6
+ # @overload is_between(range)
7
+ # Returns a boolean CArray of the same shape as `self`, `true` at each
8
+ # cell whose value lies between `lo` and `hi`. Both bounds are included
9
+ # unless `exclude:` names the one to leave out:
10
+ #
11
+ # a.is_between(2, 4) # 2 <= a <= 4
12
+ # a.is_between(2, 4, exclude: :upper) # 2 <= a < 4
13
+ # a.is_between(2, 4, exclude: :lower) # 2 < a <= 4
14
+ # a.is_between(2, 4, exclude: :both) # 2 < a < 4
15
+ #
16
+ # A Range says the same as the two bounds, its excluded end as
17
+ # `exclude: :upper`: `a.is_between(2...4)`. A bound given as `nil` (or
18
+ # the open end of `..4` / `2..`) leaves that side unbounded.
19
+ #
20
+ # It is the two comparisons joined by `&`, so it takes what they take --
21
+ # a time array is bounded by Strings, `Time` or elements -- and answers as
22
+ # they do: a masked cell is masked, and NaN is outside every interval.
23
+ # Bounds the wrong way round (`lo > hi`) contain nothing.
24
+ # @param lo [Object, nil] the lower bound.
25
+ # @param hi [Object, nil] the upper bound.
26
+ # @param exclude [nil, :lower, :upper, :both] the bounds left out.
27
+ # @return [CArray] boolean, the shape of `self`.
28
+ # @raise [ArgumentError] when neither bound is given, `exclude:` is not one
29
+ # of the three, or a Range comes with a second bound or `exclude:`.
30
+ def is_between (*bounds, exclude: nil)
31
+ if bounds.size == 1 && bounds[0].is_a?(Range)
32
+ unless exclude.nil?
33
+ raise ArgumentError,
34
+ "is_between: a Range says which ends it includes; give two bounds to use exclude:"
35
+ end
36
+ range = bounds[0]
37
+ lo, hi = range.begin, range.end
38
+ exclude = :upper if range.exclude_end? && !hi.nil?
39
+ elsif bounds.size == 2
40
+ lo, hi = bounds
41
+ else
42
+ raise ArgumentError,
43
+ "is_between: give a lower and an upper bound, or a Range (got #{bounds.size} arguments)"
44
+ end
45
+ unless [nil, :lower, :upper, :both].include?(exclude)
46
+ raise ArgumentError,
47
+ "is_between: exclude: must be :lower, :upper or :both (got #{exclude.inspect})"
48
+ end
49
+ if lo.nil? && hi.nil?
50
+ raise ArgumentError, "is_between: give at least one bound"
51
+ end
52
+ lower = lo.nil? ? nil : (%i[lower both].include?(exclude) ? gt(lo) : ge(lo))
53
+ upper = hi.nil? ? nil : (%i[upper both].include?(exclude) ? lt(hi) : le(hi))
54
+ lower && upper ? lower & upper : (lower || upper)
55
+ end
56
+
57
+ # @!endgroup
58
+
59
+ end