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
data/ext/ruby_carray.c CHANGED
@@ -44,6 +44,7 @@ void Init_carray_access ();
44
44
  void Init_carray_index_classifier ();
45
45
  void Init_carray_element ();
46
46
  void Init_carray_scatter (); /* scatter_*! family */
47
+ void Init_carray_segment (); /* segment lengths / offsets / index */
47
48
  void Init_ca_axis_group (); /* devel/MEMO_AXIS_GROUP.md — axis-group reduction kernel */
48
49
  void Init_ca_group_iter (); /* devel/MEMO_AXIS_GROUP.md — axis-group [] surface */
49
50
  void Init_carray_bincount (); /* bincount dedicated kernel */
@@ -89,6 +90,8 @@ void Init_ca_obj_byte_swap ();
89
90
  void Init_ca_face (); /* CAFace abstract base; must precede CATime / CATimedelta / CARecord / CAConstString */
90
91
  void Init_ca_source (); /* CASource abstract base; concrete sources live in C extensions */
91
92
  void Init_ca_obj_time (); /* first numeric Face */
93
+ void Init_carray_time_text ();
94
+ void Init_caframe_csv_reader ();
92
95
  void Init_ca_obj_timedelta (); /* CATime sibling */
93
96
  void Init_ca_obj_record (); /* composite Face for CAStruct backing */
94
97
  void Init_ca_obj_const_string ();/* read-only variable-length string Face */
@@ -169,7 +172,7 @@ Init_carray_ext (void)
169
172
  /* -- Exception class -- */
170
173
 
171
174
  rb_eCADataTypeError =
172
- rb_define_class_under(rb_cCArray, "DataTypeError", rb_eStandardError);
175
+ rb_define_class_under(rb_cCArray, "DataTypeError", rb_eTypeError);
173
176
 
174
177
  /* -- CA module -- namespace for misc utilities related to CArray */
175
178
 
@@ -286,6 +289,7 @@ Init_carray_ext (void)
286
289
  Init_carray_index_classifier();
287
290
  Init_carray_element();
288
291
  Init_carray_scatter();
292
+ Init_carray_segment();
289
293
  Init_ca_axis_group(); /* devel/MEMO_AXIS_GROUP.md — axis-group reduction kernel */
290
294
  Init_carray_bincount();
291
295
  Init_carray_factorize();
@@ -343,6 +347,8 @@ Init_carray_ext (void)
343
347
  (datetime / timedelta / record /
344
348
  const_string). */
345
349
  Init_ca_obj_time();
350
+ Init_carray_time_text();
351
+ Init_caframe_csv_reader();
346
352
  Init_ca_obj_timedelta();
347
353
  Init_ca_obj_record();
348
354
  Init_ca_obj_const_string();
data/ext/version.h CHANGED
@@ -1,6 +1,6 @@
1
- #define CA_VERSION "3.0.2"
2
- #define CA_VERSION_CODE 302
1
+ #define CA_VERSION "3.0.3"
2
+ #define CA_VERSION_CODE 30003
3
3
  #define CA_VERSION_MAJOR 3
4
4
  #define CA_VERSION_MINOR 0
5
- #define CA_VERSION_TEENY 2
6
- #define CA_VERSION_DATE "2026/09/24"
5
+ #define CA_VERSION_TEENY 3
6
+ #define CA_VERSION_DATE "2026/10/13"
data/ext/version.rb CHANGED
@@ -21,8 +21,10 @@ def carray_version
21
21
 
22
22
  ca_version2 = format("%i.%i.%i",
23
23
  ca_version_major, ca_version_minor, ca_version_teeny)
24
- ca_version_code2 =
25
- 100 * ca_version_major + 10*ca_version_minor + ca_version_teeny
24
+ # Two decimal digits for each of minor and teeny, so 3.0.10 (30010)
25
+ # comes after 3.0.9 (30009) and before 3.1.0 (30100).
26
+ ca_version_code2 =
27
+ 10000 * ca_version_major + 100 * ca_version_minor + ca_version_teeny
26
28
 
27
29
  # Allow pre-release suffix like "3.0.0.dev" / "3.0.0.alpha1" while still
28
30
  # validating the numeric MAJOR.MINOR.TEENY prefix.
@@ -4,6 +4,7 @@
4
4
  # Surface:
5
5
  # ca.attr(:unit) # => getter; nil if absent
6
6
  # ca.set_attr(:unit, "m/s") # => setter; writes to self's @attr
7
+ # ca.set_attrs(other.attrs) # => sets every key of a Hash at once
7
8
  # ca.attrs # => frozen shallow clone of the full Hash
8
9
  # ca.has_attr? # => any attribute present?
9
10
  # ca.has_attr?(:unit) # => specific key present?
@@ -27,6 +28,14 @@
27
28
  # `attrs` returns the effective merged Hash (deeper writes
28
29
  # override shallower ones), frozen.
29
30
  # Setter side always writes to self's @attr.
31
+ # The walk stops at a lazy operation view (CAMonOp / CABinOp /
32
+ # CATriOp / CAMonCmp / CABinCmp): its cells are computed, not the
33
+ # parent's cells, so the parent's attributes do not describe them.
34
+ # `a.lazy` itself (CALazyMarker) is a view of `a` and walks through.
35
+ #
36
+ # Copies: `copy` and `to_type` give the new array the attributes the
37
+ # source shows, as its own Hash. Arithmetic and reductions do not
38
+ # carry them -- the result is a different quantity.
30
39
 
31
40
  class CArray
32
41
 
@@ -61,6 +70,30 @@ class CArray
61
70
  (@attr ||= {})[attr_normalize_key(key)] = attr_coerce_value(value)
62
71
  end
63
72
 
73
+ # @overload set_attrs(hash)
74
+ # Sets every key of `hash` on `self`, as {#set_attr} does one key.
75
+ # Keys already on `self` and not in `hash` are kept. Every value is
76
+ # validated before any is written, so a rejected value leaves `self`
77
+ # unchanged. `b.set_attrs(a.attrs)` gives `b` the attributes `a`
78
+ # shows.
79
+ # @param hash [Hash{Symbol, String => Object}] attributes to set.
80
+ # @return [CArray] self.
81
+ # @raise [TypeError] when a key or a value is not accepted.
82
+ def set_attrs (hash)
83
+ unless hash.is_a?(Hash)
84
+ raise TypeError, "set_attrs takes a Hash (got #{hash.class})"
85
+ end
86
+ entries = hash.map do |key, value|
87
+ attr_validate_value(value)
88
+ [attr_normalize_key(key), attr_coerce_value(value)]
89
+ end
90
+ unless entries.empty?
91
+ h = (@attr ||= {})
92
+ entries.each { |k, v| h[k] = v }
93
+ end
94
+ self
95
+ end
96
+
64
97
  # @overload attrs
65
98
  # Returns a frozen shallow Hash of all attributes visible on
66
99
  # `self`, merged along the parent chain (deeper writes shadow
@@ -114,6 +147,7 @@ class CArray
114
147
  h = ca.instance_variable_get(:@attr)
115
148
  yield(h) if h
116
149
  end
150
+ return nil if attr_computing_view?(ca)
117
151
  parent = ca.respond_to?(:parent) ? ca.parent : nil
118
152
  return nil if parent.nil? || parent.equal?(ca)
119
153
  ca = parent
@@ -121,6 +155,11 @@ class CArray
121
155
  nil
122
156
  end
123
157
 
158
+ def attr_computing_view? (ca)
159
+ ca.is_a?(CAMonOp) || ca.is_a?(CABinOp) || ca.is_a?(CATriOp) ||
160
+ ca.is_a?(CAMonCmp) || ca.is_a?(CABinCmp)
161
+ end
162
+
124
163
  def attr_normalize_key (key)
125
164
  case key
126
165
  when String then key
@@ -47,6 +47,23 @@ class CArray
47
47
  autoload_method "marshal_load", serialize_rb
48
48
  end
49
49
 
50
+ # A view, a wrap and a scalar are marshalled through _dump / self._load,
51
+ # which return a new array: with marshal_dump Marshal records the class and
52
+ # loads into an empty instance of it, which the plain array the payload
53
+ # holds cannot become. A record array keeps marshal_dump, under which it
54
+ # already round-trips with its class.
55
+ [CAView, CAWrap, CScalar].each do |klass|
56
+ klass.class_eval do
57
+ undef_method :marshal_dump
58
+ autoload_method "_dump", "carray/serialize"
59
+ autoload_method "self._load", "carray/serialize"
60
+ end
61
+ end
62
+
63
+ class CARecord
64
+ autoload_method "marshal_dump", "carray/serialize"
65
+ end
66
+
50
67
  # ---- Arrow tensor IPC (interop) --------------------------------------------
51
68
 
52
69
  class CArray
@@ -126,10 +143,12 @@ class CArray
126
143
  categorical_iterator_rb = "carray/categorical_iterator"
127
144
  autoload_method "group_by_category", categorical_iterator_rb
128
145
  autoload_method "group_by_run", categorical_iterator_rb
146
+ autoload_method "segments", "carray/segment_iterator"
129
147
  end
130
148
 
131
149
  autoload :CACategorical, "carray/categorical"
132
150
  autoload :CACategoricalIterator, "carray/categorical_iterator"
151
+ autoload :CASegmentIterator, "carray/segment_iterator"
133
152
  autoload :AxisGroup, "carray/axis_group"
134
153
  autoload :GroupLabels, "carray/axis_group"
135
154
 
@@ -53,5 +53,11 @@ module AutoloadMethodExtension
53
53
  send(name, *args, **kwargs, &block)
54
54
  end
55
55
  stub = target.instance_method(name)
56
+ # The library replaces the stub with a plain `def`, which `ruby -w`
57
+ # reports as a redefinition. Replacing is what the stub is for, so mark
58
+ # it as aliased: Ruby does not warn when the old definition has an alias.
59
+ tmp = :"__autoload_stub_#{name}__"
60
+ target.send(:alias_method, tmp, name)
61
+ target.send(:remove_method, tmp)
56
62
  end
57
63
  end
@@ -213,6 +213,57 @@ class AxisGroup
213
213
  end
214
214
  end
215
215
 
216
+ # The keywords a group reduction takes besides axis:. Checked before the
217
+ # work, so an unknown keyword is refused rather than dropped.
218
+ REDUCE_KEYWORDS = [:axis, :min_count, :fill_value].freeze
219
+
220
+ # all / any take axis: only, as the core ones do.
221
+ LOGICAL_REDUCE_KEYWORDS = [:axis].freeze
222
+
223
+ def self.__check_reduce_keywords__ (op, kw)
224
+ allowed = [:all, :any].include?(op.to_sym) ? LOGICAL_REDUCE_KEYWORDS : REDUCE_KEYWORDS
225
+ unknown = kw.keys - allowed
226
+ unless unknown.empty?
227
+ raise ArgumentError,
228
+ "#{op}: unknown keyword#{unknown.size > 1 ? "s" : ""}: " +
229
+ unknown.map(&:inspect).join(", ")
230
+ end
231
+ CArray.__min_count_need__(op, kw[:min_count], 0)
232
+ nil
233
+ end
234
+
235
+ # A boolean array carrying value's mask: what a count reads, for a value
236
+ # whose payload the group kernel cannot read.
237
+ def self.__count_stand_in__ (value)
238
+ return value if value.boolean? || value.integer? || value.float?
239
+ stand_in = CArray.boolean(*value.shape)
240
+ stand_in.mask = value.mask if value.has_mask?
241
+ stand_in
242
+ end
243
+
244
+ # A count of the mask reads every cell of the group, as the core's does.
245
+ MASK_COUNTS = [:count, :count_not_masked, :count_masked].freeze
246
+
247
+ # min_count: masks a group with fewer contributing cells (present cells,
248
+ # or every cell for a count of the mask); fill_value: then fills the masked
249
+ # cells -- as a core reduction does with each.
250
+ def self.__finish_reduction__ (iter, op, kw, result)
251
+ need = CArray.__min_count_need__(op, kw[:min_count], 0)
252
+ if need > 0
253
+ counts = if MASK_COUNTS.include?(op.to_sym)
254
+ iter.elements(axis: kw[:axis])
255
+ else
256
+ iter.count_not_masked(axis: kw[:axis])
257
+ end
258
+ if result.is_a?(CArray)
259
+ result[counts.lt(need)] = UNDEF
260
+ elsif counts < need
261
+ result = UNDEF
262
+ end
263
+ end
264
+ CArray.__reduce_fill__(result, kw[:fill_value])
265
+ end
266
+
216
267
  # Parse an axis: spec into [has_group, fused_band_slot_positions].
217
268
  def self.parse_axis (axis)
218
269
  has_group = false
@@ -348,11 +399,9 @@ class CAGroupIterator
348
399
  end
349
400
  v = args.first
350
401
  return count_masked(**kw) if v.equal?(UNDEF)
351
- # a masked cell equals nothing, and eq marks it UNDEF; drop that to false
352
- # so it is simply not counted
353
- hit = value.eq(v)
354
- hit = hit.strip_mask(false) if hit.has_mask?
355
- self.class.__build__(hit.int64, spec).sum(**kw).int64
402
+ # eq leaves a masked cell UNDEF, so the sum leaves it out and its
403
+ # min_count: counts the present cells, as the core count(v) does
404
+ self.class.__build__(value.eq(v).int64, spec).sum(**kw).int64
356
405
  end
357
406
 
358
407
  alias __all_folded__ all
@@ -383,7 +432,30 @@ class CAGroupIterator
383
432
 
384
433
  # Per-group count of value-masked cells = elements - count_not_masked.
385
434
  def count_masked (**kw)
386
- elements(**kw) - count_not_masked(**kw)
435
+ has_group, = AxisGroup.parse_axis(kw[:axis])
436
+ return value.count_masked(**kw) unless has_group
437
+ AxisGroup.__check_reduce_keywords__(:count_masked, kw)
438
+ plain = { axis: kw[:axis] }
439
+ AxisGroup.__finish_reduction__(self, :count_masked, kw,
440
+ elements(**plain) - count_not_masked(**plain))
441
+ end
442
+
443
+ # The scatter kernel reads an integer or float payload, and answers there
444
+ # as the core does. Any other payload -- boolean, complex, object, a Face --
445
+ # is reduced group by group by the core, through the composite categorical,
446
+ # so the data type, the Face and a refusal are the core's: a boolean's sum
447
+ # is uint64, a time column's minimum a time, its sum refused.
448
+ [:sum, :accumulate, :prod, :mean, :min, :max,
449
+ :variance, :stddev, :variancep, :stddevp].each do |op|
450
+ alias_method :"__#{op}_by_kernel__", op
451
+ define_method(op) do |**kw|
452
+ has_group, = AxisGroup.parse_axis(kw[:axis])
453
+ if ! has_group || kernel_reads_value?
454
+ __send__(:"__#{op}_by_kernel__", **kw)
455
+ else
456
+ reduce_by_core(op, kw)
457
+ end
458
+ end
387
459
  end
388
460
 
389
461
  # Per-group [min, max] pair (matching CArray#minmax).
@@ -427,13 +499,21 @@ class CAGroupIterator
427
499
  # (no present pair) is UNDEF (matching CArray#wmean); a present group whose
428
500
  # weights sum to zero yields NaN/Inf (core's 0/0).
429
501
  def wmean (weights, **kw)
502
+ has_group, = AxisGroup.parse_axis(kw[:axis])
503
+ return value.wmean(weights, **kw) unless has_group
504
+ AxisGroup.__check_reduce_keywords__(:wmean, kw)
505
+ plain = { axis: kw[:axis] }
430
506
  prod = value * weights
431
- num = self.class.__build__(prod, spec).sum(**kw)
432
- den = self.class.__build__(value * 0 + weights, spec).sum(**kw)
433
- cnt = self.class.__build__(prod, spec).count_not_masked(**kw)
434
- out = num / den
435
- out[cnt.eq(0)] = UNDEF # no present pair -> masked (empty group)
436
- out
507
+ num = self.class.__build__(prod, spec).sum(**plain)
508
+ den = self.class.__build__(value * 0 + weights, spec).sum(**plain)
509
+ cnt = self.class.__build__(prod, spec).count_not_masked(**plain)
510
+ # No present pair is UNDEF (an empty group), set on the denominator so
511
+ # the division never meets an empty group's 0 / 0.
512
+ den = den.copy
513
+ den[cnt.eq(0)] = UNDEF
514
+ # min_count: counts the present pairs, as the core wmean does.
515
+ pairs = self.class.__build__(prod, spec)
516
+ AxisGroup.__finish_reduction__(pairs, :wmean, kw, num / den)
437
517
  end
438
518
 
439
519
  # ---- segment scan: within-group running statistics ---------------------
@@ -569,12 +649,13 @@ class CAGroupIterator
569
649
  raise LocalJumpError, "no block given (yield)" unless block
570
650
  ccat, _kd, gslots, bslots, gaxes = composite_layout
571
651
  return value.group_by_category(ccat).map(data_type: data_type, &block) if bslots.empty?
572
- dt = data_type || value.data_type
573
- out = CArray.new(dt, value.shape)
652
+ out = nil
574
653
  each_band_block(ccat, gslots, bslots, gaxes) do |val_idx, _oi, _co, gi|
575
- out[*val_idx] = gi.map(data_type: dt, &block)
654
+ result = gi.map(data_type: data_type, &block)
655
+ out ||= blank_of(result, value.shape)
656
+ out[*val_idx] = result
576
657
  end
577
- out
658
+ out || (data_type ? CArray.new(data_type, value.shape) : blank_of(value, value.shape))
578
659
  end
579
660
 
580
661
  # @overload reduce { |members| ... }
@@ -622,7 +703,7 @@ class CAGroupIterator
622
703
  # askable. Asking a one-cell array of the same data type lets the core's own
623
704
  # refusal through, unworded by us.
624
705
  def boolean_payload! (op)
625
- CArray.new(value.data_type, [1]).public_send(op)
706
+ CArray.new(value.data_type, [1], bytes: value.bytes).public_send(op)
626
707
  nil
627
708
  end
628
709
 
@@ -696,10 +777,71 @@ class CAGroupIterator
696
777
  "axis_group scan: folding a band into a scan (axis: [:group, k]) " \
697
778
  "is not supported; a scan preserves the source shape"
698
779
  end
780
+ return scan_by_core(op) unless kernel_scans?(op)
699
781
  group_axes, bundles, = spec.reduce_plan([])
700
782
  value.__axis_group_scan__(group_axes, bundles, op)
701
783
  end
702
784
 
785
+ # Whether the scatter kernels read this payload: an integer or a float,
786
+ # not a Face. They answer there in the core's data types.
787
+ def kernel_reads_value?
788
+ ! value.face? && (value.integer? || value.float?)
789
+ end
790
+
791
+ # Whether the scan kernel gives the core's answer: it reads an integer,
792
+ # float, boolean or object payload (not a complex, not a Face), and answers cumsum / cumprod in
793
+ # float64 (an object in object), cummax / cummin in the payload's data type
794
+ # and cumcount in int64 -- not the core's answer for a boolean's running sum.
795
+ def kernel_scans? (op)
796
+ return false if value.face? || value.complex?
797
+ return true if value.object? || op == :cumcount
798
+ core = CArray.new(value.data_type, [1], bytes: value.bytes).public_send(op).data_type
799
+ kernel = [:cumsum, :cumprod].include?(op) ? CA_FLOAT64 : value.data_type
800
+ core == kernel
801
+ end
802
+
803
+ # A scan the kernel does not answer as the core does: each group block is
804
+ # grouped through the composite categorical, whose scans are the core's.
805
+ def scan_by_core (op)
806
+ ccat, _kd, gslots, bslots, gaxes = composite_layout
807
+ return value.group_by_category(ccat).public_send(op) if bslots.empty?
808
+ out = nil
809
+ each_band_block(ccat, gslots, bslots, gaxes) do |val_idx, _oi, _co, gi|
810
+ result = gi.public_send(op)
811
+ out ||= blank_of(result, value.shape)
812
+ out[*val_idx] = result
813
+ end
814
+ out || blank_of(value, value.shape)
815
+ end
816
+
817
+ # A reduction of a payload the scatter kernel does not read, group by
818
+ # group through the composite categorical. Folding a band into the
819
+ # statistic needs the kernel.
820
+ def reduce_by_core (op, kw)
821
+ _, fused = AxisGroup.parse_axis(kw[:axis])
822
+ unless fused.empty?
823
+ raise ArgumentError,
824
+ "axis_group #{op}: folding a band into the statistic " \
825
+ "(axis: [:group, k]) is not supported for a #{value.class} of " \
826
+ "data type #{value.data_type_name}"
827
+ end
828
+ AxisGroup.__check_reduce_keywords__(op, kw)
829
+ AxisGroup.__finish_reduction__(self, op, kw, composite_order(op, []))
830
+ end
831
+
832
+ # An array of +shape+ of the same kind as +model+ -- its data type, or its
833
+ # Face -- with every cell UNDEF. A read-only Face cannot be written cell by
834
+ # cell, so its values are held in an object array.
835
+ def blank_of (model, shape)
836
+ out = if model.face? && model.read_only?
837
+ CArray.new(CA_OBJECT, shape)
838
+ else
839
+ model.__send__(:__blank_like__, shape)
840
+ end
841
+ out[] = UNDEF
842
+ out
843
+ end
844
+
703
845
  # Dispatch an order statistic (median / percentile / quantile). No :group ->
704
846
  # plain value reduction; :group -> per-group order statistics via the composite
705
847
  # materialize. Folding a band into an order statistic (axis: [:group, k])
@@ -707,13 +849,19 @@ class CAGroupIterator
707
849
  # operation, and remains a follow-up.
708
850
  def order_stat (op, args, kw)
709
851
  has_group, fused = AxisGroup.parse_axis(kw[:axis])
710
- return value.public_send(op, *args) unless has_group
852
+ return value.public_send(op, *args, **kw) unless has_group
711
853
  unless fused.empty?
712
854
  raise NotImplementedError,
713
855
  "axis_group: folding a band into an order statistic (axis: [:group, k]) " \
714
856
  "is not supported; order statistics gather every member of a group"
715
857
  end
716
- composite_order(op, args)
858
+ AxisGroup.__check_reduce_keywords__(op, kw)
859
+ res = composite_order(op, args)
860
+ if op == :quantile
861
+ res.map { |r| AxisGroup.__finish_reduction__(self, op, kw, r) }
862
+ else
863
+ AxisGroup.__finish_reduction__(self, op, kw, res)
864
+ end
717
865
  end
718
866
 
719
867
  # Per-group order statistic over the composite grouping. A flat grouping (no
@@ -732,15 +880,15 @@ class CAGroupIterator
732
880
  return nout == 1 ? res.reshape(*kdims) : res.map { |r| r.reshape(*kdims) }
733
881
  end
734
882
  out_shape = spec.slot_meta.map { |m| m[:kind] == :group ? m[:k] : m[:len] }
735
- outs = Array.new(nout) { CArray.float64(*out_shape) }
883
+ outs = nil
736
884
  each_band_block(ccat, gslots, bslots, gaxes) do |_vi, out_idx, _co, gi|
737
- result = gi.public_send(op, *args)
738
- if nout == 1
739
- outs[0][*out_idx] = result.reshape(*kdims)
740
- else
741
- result.each_with_index { |r, q| outs[q][*out_idx] = r.reshape(*kdims) }
742
- end
885
+ results = gi.public_send(op, *args)
886
+ results = [results] unless results.is_a?(Array) # quantile answers five
887
+ # each output takes the kind the categorical answered in
888
+ outs ||= results.map { |r| blank_of(r, out_shape) }
889
+ results.each_with_index { |r, q| outs[q][*out_idx] = r.reshape(*kdims) }
743
890
  end
891
+ outs ||= Array.new(nout) { blank_of(value, out_shape) }
744
892
  nout == 1 ? outs[0] : outs
745
893
  end
746
894
 
data/lib/carray/basics.rb CHANGED
@@ -80,31 +80,31 @@ class CArray
80
80
  Array.new(flat.length, repeat)
81
81
  end
82
82
 
83
- # Validate each value. A positive Integer only; nil is not a valid
84
- # per-axis repeat.
85
- reps.each do |r|
86
- unless r.is_a?(Integer)
87
- raise ArgumentError,
88
- "insert_axis: repeat must be a positive Integer, got #{r.inspect}"
83
+ # Validate each value. A positive Integer only; nil is not a valid
84
+ # per-axis repeat.
85
+ reps.each do |r|
86
+ unless r.is_a?(Integer)
87
+ raise ArgumentError,
88
+ "insert_axis: repeat must be a positive Integer, got #{r.inspect}"
89
+ end
90
+ raise ArgumentError, "insert_axis: repeat count must be >= 1" if r < 1
89
91
  end
90
- raise ArgumentError, "insert_axis: repeat count must be >= 1" if r < 1
91
- end
92
92
 
93
- # Final output layout: stable order by (gap, argument index) keeps
94
- # same-gap axes in argument order; the k-th inserted axis lands at output
95
- # position gap + k. This output position only drives broadcast_to; the
96
- # insertion itself always goes through the source-frame primitive.
97
- order = (0...flat.length).sort_by { |i| [gaps[i], i] }
98
- final = {}
99
- order.each_with_index { |i, k| final[i] = gaps[i] + k }
93
+ # Final output layout: stable order by (gap, argument index) keeps
94
+ # same-gap axes in argument order; the k-th inserted axis lands at output
95
+ # position gap + k. This output position only drives broadcast_to; the
96
+ # insertion itself always goes through the source-frame primitive.
97
+ order = (0...flat.length).sort_by { |i| [gaps[i], i] }
98
+ final = {}
99
+ order.each_with_index { |i, k| final[i] = gaps[i] + k }
100
100
 
101
- inter = __insert_axis_size1__(*order.map { |i| gaps[i] })
102
- return inter unless order.any? { |i| reps[i] > 1 }
101
+ inter = __insert_axis_size1__(*order.map { |i| gaps[i] })
102
+ return inter unless order.any? { |i| reps[i] > 1 }
103
103
 
104
- shp = inter.shape
105
- order.each { |i| shp[final[i]] = reps[i] if reps[i] > 1 }
106
- inter.broadcast_to(*shp)
107
- end
104
+ shp = inter.shape
105
+ order.each { |i| shp[final[i]] = reps[i] if reps[i] > 1 }
106
+ inter.broadcast_to(*shp)
107
+ end
108
108
 
109
109
  # @overload drop_axis
110
110
  # Returns a view of `self` with every size-1 axis dropped.
@@ -142,6 +142,28 @@ end
142
142
  return template(:boolean) { 1 }
143
143
  end
144
144
 
145
+ # A Face is clipped in its own terms: its comparisons decide which cells are
146
+ # out of range (a time array reads its bounds as times, a record array
147
+ # orders by its declared members, a Face that cannot be ordered refuses),
148
+ # and its store writes the bound or the fill value into them.
149
+ private def __face_clip__ (min, max, fill_value, lfill, ufill)
150
+ lfill = fill_value if lfill.nil?
151
+ ufill = fill_value if ufill.nil?
152
+ out = copy
153
+ out[self < min] = lfill.nil? ? min : lfill unless min.nil?
154
+ out[self > max] = ufill.nil? ? max : ufill unless max.nil?
155
+ out
156
+ end
157
+
158
+ # An array of +shape+ of the same kind as self: its data type and cell
159
+ # width, or its Face over a zero storage. Nothing is written into it, so
160
+ # it can be built for a read-only Face too.
161
+ private def __blank_like__ (shape)
162
+ storage = __face_storage__
163
+ out = CArray.new(storage.data_type, shape, bytes: storage.bytes)
164
+ face? ? out.face_lift(self) : out
165
+ end
166
+
145
167
  # Sub-region copy (paste / crop), the old C `ca_paste` / `ca_clip` pair.
146
168
  # `clip` is reserved for value clamping (clamp to a min/max range), so
147
169
  # the read-out side is `crop`. Kept together as a pair (crop is rare, but paste is reached
@@ -250,6 +272,31 @@ end
250
272
 
251
273
  # @!endgroup
252
274
 
275
+ # An Integer shift count on an integer array means what it means to
276
+ # Integer#<< and #>>: negative shifts the other way, and a count past the
277
+ # width shifts every bit out. The kernels do that for a count of the
278
+ # array's own type; this brings a count the type cannot hold -- -1 for a
279
+ # uint8 array, 300 for an int8 one -- to one it can.
280
+ module IntegerShiftCount
281
+ UNSIGNED = [CA_UINT8, CA_UINT16, CA_UINT32, CA_UINT64].freeze
282
+
283
+ def << (other)
284
+ return super unless other.is_a?(Integer) && integer? && ! face?
285
+ w = bytes * 8
286
+ return self >> -other if other < 0 && UNSIGNED.include?(data_type)
287
+ super(other.clamp(-w, w))
288
+ end
289
+
290
+ def >> (other)
291
+ return super unless other.is_a?(Integer) && integer? && ! face?
292
+ w = bytes * 8
293
+ return self << -other if other < 0 && UNSIGNED.include?(data_type)
294
+ super(other.clamp(-w, w))
295
+ end
296
+ end
297
+ # Prepended by carray/lazy, after it has wrapped << and >>: prepended
298
+ # here, the method lazy aliases as the eager form would be this one.
299
+
253
300
  # @overload clip(min, max = nil, fill_value = nil, lfill: nil, ufill: nil)
254
301
  # Returns `self` with every element clamped to `[min, max]`.
255
302
  #
@@ -258,7 +305,12 @@ end
258
305
  # `fill_value` (or `lfill` / `ufill`) is given, out-of-range
259
306
  # cells are replaced by the fill instead of clamped -- pass
260
307
  # `UNDEF` to mask that end. `fill_value` is sugar for symmetric
261
- # dual-fill; `lfill` / `ufill` override per side.
308
+ # dual-fill; `lfill` / `ufill` override per side, and a side
309
+ # given no fill is still clamped.
310
+ #
311
+ # `min` may not exceed `max` (in any cell, when a bound is an
312
+ # array); equal bounds give every unmasked cell that value.
313
+ # A NaN bound bounds nothing.
262
314
  #
263
315
  # Boundary is strict `[min, max]`: values equal to a bound
264
316
  # remain unchanged in both the clamped and filled variants.
@@ -272,7 +324,8 @@ end
272
324
  # @param lfill [Object, nil] override below-range fill.
273
325
  # @param ufill [Object, nil] override above-range fill.
274
326
  # @return [CArray] new CArray with clamped or filled values.
275
- # @raise [ArgumentError] when both `min` and `max` are `nil`.
327
+ # @raise [ArgumentError] when both `min` and `max` are `nil`,
328
+ # or when `min` exceeds `max`.
276
329
  # @example
277
330
  # a.clip(0, 10) # strict clamp
278
331
  # a.clip(0, 10, -1) # both ends -> -1
@@ -281,6 +334,16 @@ end
281
334
  if min.nil? && max.nil?
282
335
  raise ArgumentError, "clip: at least one of (min, max) must be given"
283
336
  end
337
+ return __face_clip__(min, max, fill_value, lfill, ufill) if face?
338
+ if !min.nil? && !max.nil?
339
+ reversed = if min.is_a?(CArray) then min.gt(max).any
340
+ elsif max.is_a?(CArray) then max.lt(min).any
341
+ else min > max
342
+ end
343
+ if reversed
344
+ raise ArgumentError, "clip: min must be less than or equal to max"
345
+ end
346
+ end
284
347
 
285
348
  # `fill_value` as a single argument is sugar applied to both ends; kwargs override.
286
349
  lfill = fill_value if lfill.nil?
@@ -292,9 +355,12 @@ end
292
355
  return pmin(max)
293
356
  end
294
357
 
358
+ # A side without a fill still clamps.
295
359
  out = self.copy
296
- out[:lt, min] = lfill unless min.nil? || lfill.nil?
297
- out[:gt, max] = ufill unless max.nil? || ufill.nil?
360
+ out = out.pmax(min) if !min.nil? && lfill.nil?
361
+ out = out.pmin(max) if !max.nil? && ufill.nil?
362
+ out[self.lt(min)] = lfill unless min.nil? || lfill.nil?
363
+ out[self.gt(max)] = ufill unless max.nil? || ufill.nil?
298
364
  out
299
365
  end
300
366
 
@@ -269,6 +269,12 @@ class CArray
269
269
  term = strides_ext[k] == 1 ? b : b * strides_ext[k]
270
270
  ravel = ravel.nil? ? term : ravel + term
271
271
  end
272
+ # A NaN weight drops its sample, as the fiber kernel and histogram
273
+ # do; bincount itself would add the NaN in.
274
+ if tweights && tweights.float?
275
+ nan = tweights.is_nan
276
+ tweights = tweights.mask_where(nan) if nan.any
277
+ end
272
278
  chunk_counts = ravel.bincount(weights: tweights, length: total_ext)
273
279
  @full_counts[] = @full_counts + chunk_counts.reshape(*@full_counts.shape)
274
280
  else