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
@@ -0,0 +1,990 @@
1
+ # ----------------------------------------------------------------------------
2
+ #
3
+ # carray/segment_iterator.rb
4
+ #
5
+ # CASegmentIterator -- reductions over consecutive segments of a flat
6
+ # sequence. The pieces are contiguous runs of a held, contiguous copy
7
+ # (`grouped`), cut at the boundaries in @bounds (every boundary, k+1) and
8
+ # @offsets (the starts, k, the form the reduceat kernels read). Each
9
+ # reduction is the core reduction lifted to the piece.
10
+ #
11
+ # CACategoricalIterator is this iterator over a category-sorted copy: it
12
+ # builds `grouped` by a counting sort and adds the labels and the
13
+ # per-fiber (axis:) form. A segment iterator has no axis: form; the
14
+ # hooks at the bottom say so where a reduction is asked for one.
15
+ #
16
+ # ----------------------------------------------------------------------------
17
+
18
+ require "carray"
19
+
20
+ # A CAIterator over consecutive segments of a flat sequence, built by
21
+ # {CArray#segments}. Segment `c` is the cells `offsets[c]...offsets[c + 1]`
22
+ # of the value read in flatten order, and each reduction answers one value
23
+ # per segment -- the core reduction of the same name, lifted to the segment,
24
+ # with the core's data types and its answer for an empty or all-masked
25
+ # segment. The pieces never overlap, so {#map} and the running scans are
26
+ # available too.
27
+ #
28
+ # The iterator holds a copy of the covered cells, taken at construction.
29
+ # Cells outside `offsets[0]...offsets[-1]` belong to no segment: they are
30
+ # UNDEF in {#map} and in the scans.
31
+ class CASegmentIterator < CAIterator
32
+
33
+ # value : the CArray whose segments are reduced, read in flatten order.
34
+ # offsets : the k+1 boundaries (segment c spans offsets[c]...offsets[c+1]),
35
+ # lengths : or the k lengths, the segments then starting at cell 0.
36
+ #
37
+ # Takes a copy of the cells the segments cover, so the reductions answer
38
+ # about the values as they were at construction. Cells before offsets[0]
39
+ # or from offsets[-1] on belong to no segment.
40
+ def initialize (value, offsets: nil, lengths: nil)
41
+ kw = {}
42
+ kw[:offsets] = offsets unless offsets.nil?
43
+ kw[:lengths] = lengths unless lengths.nil?
44
+ bounds = CArray.__segment_bounds__("segments", **kw)
45
+ origin, finish = bounds[0], bounds[-1]
46
+ if origin < 0 || finish > value.elements
47
+ raise ArgumentError,
48
+ "segments: the segments span #{origin}...#{finish}, " \
49
+ "outside the #{value.elements} elements of the value"
50
+ end
51
+ @value = value
52
+ @src_shape = value.shape
53
+ @k = bounds.elements - 1
54
+ @ndim = 1
55
+ @shape = [@k]
56
+ @origin = origin
57
+ @bounds = bounds - origin # every boundary, from 0
58
+ @offsets = @bounds[0...-1].copy # the starts, for the kernels
59
+ @elements = @k > 0 ? (@bounds[1..-1] - @bounds[0...-1]) : CArray.int64(0)
60
+ @grouped = finish > origin ? value.reshape(value.elements)[origin...finish].copy
61
+ : CArray.new(value.data_type, [0], bytes: value.bytes)
62
+ @empty = CArray.new(@grouped.data_type, [0], bytes: @grouped.bytes)
63
+ end
64
+
65
+ # @overload inspect
66
+ # Returns a one-line summary: the number of segments and their lengths.
67
+ # @return [String]
68
+ def inspect
69
+ "#<#{self.class} segments=#{@k} elements=#{@elements.to_a.inspect}>"
70
+ end
71
+
72
+ # @overload each { |members| ... }
73
+ # Yields each segment's members in order, as a CArray slice of the held
74
+ # copy; an empty segment yields an empty array. Without a block, returns
75
+ # an Enumerator.
76
+ # @yieldparam members [CArray]
77
+ # @return [Enumerator, self]
78
+ def each
79
+ return to_enum(:each) unless block_given?
80
+ @k.times { |c| yield segment_slice(c) }
81
+ self
82
+ end
83
+
84
+ # @overload elements
85
+ # Returns the number of cells in each segment, masked or not. The
86
+ # CAIterator count-family member — `CArray#elements` (structural,
87
+ # mask-independent) lifted per segment.
88
+ # @return [CArray]
89
+ def elements
90
+ # A copy, like every other member that reads off a memo: the memo is the
91
+ # iterator's own state, and handing out the array itself lets a caller
92
+ # write into it and change what the iterator answers from then on. This
93
+ # one invites it -- the docs point at its prefix sum for splitting a
94
+ # column apart, which reads as scratch.
95
+ counts.copy
96
+ end
97
+
98
+ # @overload count_not_masked
99
+ # Returns the per-segment count of present (non-masked) values as int64
100
+ # — the denominator the value reductions actually divide by. Equals
101
+ # {#elements} unless the value carries a mask. A count is always defined,
102
+ # so an empty segment is `0` (never masked).
103
+ # @return [CArray]
104
+ def count_not_masked(axis: nil)
105
+ return axis_counts(axis, :count_not_masked) if axis
106
+ m = moments
107
+ m ? m[:count].copy : per_segment(CA_INT64) { |s| s.count_not_masked }
108
+ end
109
+
110
+ # @overload count(v = <none>)
111
+ # Per-segment count, mirroring `CArray#count` per segment. No argument
112
+ # returns {#count_not_masked} (present cells); `count(UNDEF)` returns
113
+ # {#count_masked}; `count(v)` counts cells whose value equals `v`.
114
+ # @return [CArray] length-k int64
115
+ def count (*args, axis: nil)
116
+ if axis
117
+ return count_not_masked(axis: axis) if args.empty?
118
+ raise NotImplementedError,
119
+ "#{self.class}#count(v, axis:) is not implemented — " \
120
+ "value-equality count is available without axis:."
121
+ end
122
+ return count_not_masked if args.empty?
123
+ # Delegate per segment to CArray#count (handles count(UNDEF) -> masked count and
124
+ # count(v) alike, with core's exact data type equality). The segment slice is a
125
+ # CABlock, whose own #count is the block geometry accessor, so dispatch
126
+ # CArray#count explicitly. (Not fused: a value-equality reduceat would have
127
+ # to reproduce core's cross-type / out-of-range equality exactly.)
128
+ cnt = CArray.instance_method(:count)
129
+ per_segment(CA_INT64) { |s| cnt.bind_call(s, *args) }
130
+ end
131
+
132
+ # @overload count_masked
133
+ # Returns the per-segment count of masked (missing) values as int64.
134
+ # Empty segments are `0`.
135
+ # @return [CArray]
136
+ def count_masked(axis: nil)
137
+ if axis
138
+ raise NotImplementedError,
139
+ "#{self.class}#count_masked(axis:) is not implemented — " \
140
+ "call it without axis:."
141
+ end
142
+ m = moments
143
+ m ? counts - m[:count] : per_segment(CA_INT64) { |s| s.count_masked }
144
+ end
145
+
146
+ # @overload sum
147
+ # Returns per-segment sums in the data type `CArray#sum` promotes the value
148
+ # to (float64 for an integer value). `accumulate` is the same fold kept in
149
+ # the value's own type. An empty or fully-masked segment sums the empty
150
+ # set, which is the additive identity `0` (unmasked) — the same contract as
151
+ # `CArray#sum` on an empty / all-masked array.
152
+ # @return [CArray]
153
+ def sum(axis: nil)
154
+ return axis_sum(axis) if axis
155
+ m = moments
156
+ return per_segment(core_reduce_type(:sum)) { |s| s.sum } unless m
157
+ m[:sum].copy # the moments sum IS the core fold (empty -> 0.0)
158
+ end
159
+
160
+ # @overload accumulate
161
+ # Returns per-segment sums folded in the value's own data type, wrapping at
162
+ # its width, as the core `accumulate` does. This is the exact in-type fold:
163
+ # `sum` reads its answer off a float64 moment and casts back, so it loses
164
+ # the low bits of a wide integer payload and does not wrap. An empty or
165
+ # fully-masked segment accumulates the empty set, the additive identity `0`
166
+ # (unmasked).
167
+ # @return [CArray]
168
+ def accumulate(axis: nil)
169
+ return axis_by_masked_copy(axis, :accumulate, core_reduce_type(:accumulate)) if axis
170
+ per_segment(core_reduce_type(:accumulate)) { |s| s.accumulate }
171
+ end
172
+
173
+ # @overload max
174
+ # Returns per-segment maxima in the value data type. Empty segments are
175
+ # MASKED.
176
+ # @return [CArray]
177
+ def max(axis: nil)
178
+ return axis_moments(axis, :max)[:max] if axis
179
+ m = moments
180
+ m ? m[:max].copy : per_segment(core_reduce_type(:max)) { |s| s.max }
181
+ end
182
+
183
+ # @overload min
184
+ # Returns per-segment minima in the value data type. Empty segments are
185
+ # MASKED.
186
+ # @return [CArray]
187
+ def min(axis: nil)
188
+ return axis_moments(axis, :min)[:min] if axis
189
+ m = moments
190
+ m ? m[:min].copy : per_segment(core_reduce_type(:min)) { |s| s.min }
191
+ end
192
+
193
+ # @overload mean
194
+ # Returns per-segment means as float64. Empty segments are MASKED.
195
+ # @return [CArray]
196
+ def mean(axis: nil)
197
+ return axis_mean(axis) if axis
198
+ m = moments
199
+ return per_segment(core_reduce_type(:mean)) { |s| s.mean } unless m
200
+ cnt = m[:count]
201
+ out = m[:sum] / cnt.float64 # count 0 -> NaN, masked next
202
+ out[cnt.eq(0)] = UNDEF # empty / all-masked category -> MASKED
203
+ out
204
+ end
205
+
206
+ # @overload median
207
+ # Returns per-segment medians as float64. Empty segments are MASKED.
208
+ # @return [CArray]
209
+ def median(axis: nil)
210
+ return axis_order_stat(axis, :median) if axis
211
+ percentile(50.0)
212
+ end
213
+
214
+ # @overload percentile(p)
215
+ # Returns the per-segment `p`-th percentile as float64 (`p` in 0..100,
216
+ # `:linear` interpolation, matching `CArray#percentile`). Empty segments
217
+ # are MASKED. Order statistics need every value of a segment held together —
218
+ # this is the reduceat that only the eager grouped copy can serve.
219
+ # @param p [Numeric] percentile in 0..100.
220
+ # @return [CArray]
221
+ def percentile (p, axis: nil)
222
+ return axis_order_stat(axis, :percentile, p) if axis
223
+ unless MONOID_TYPES.include?(grouped.data_type)
224
+ return per_segment(core_reduce_type(:percentile, p)) { |s| s.percentile(p) }
225
+ end
226
+ out = CArray.float64(@k)
227
+ grouped.send(:__reduceat_percentile__, @offsets, p.to_f, out)
228
+ out
229
+ end
230
+
231
+ # @overload quantile
232
+ # Returns the per-segment five-number summary `[min, Q1, median, Q3, max]`
233
+ # as five length-k float64 CArrays (matching `CArray#quantile`): the
234
+ # percentiles at 0 / 25 / 50 / 75 / 100. Empty / all-masked segments are
235
+ # MASKED. For a single fraction q in 0..1 use `percentile(q * 100)`.
236
+ # @return [Array<CArray>]
237
+ def quantile
238
+ unless MONOID_TYPES.include?(grouped.data_type)
239
+ return CArray.__answer_as__(:quantile) { [0, 25, 50, 75, 100].map { |p| percentile(p) } }
240
+ end
241
+ outs = Array.new(5) { CArray.float64(@k) }
242
+ grouped.send(:__reduceat_quantile__, @offsets, *outs)
243
+ outs
244
+ end
245
+
246
+ # @overload variance
247
+ # Returns per-segment SAMPLE variance (ddof=1) as float64. Matches
248
+ # `CArray#variance` per segment: a segment with fewer than two present
249
+ # values is MASKED, n>=2 is the sample variance.
250
+ # @return [CArray]
251
+ def variance(axis: nil)
252
+ return axis_by_masked_copy(axis, :variance) if axis
253
+ m = moments
254
+ return per_segment(core_reduce_type(:variance)) { |s| s.variance } unless m
255
+ cnt = m[:count]
256
+ means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1,
257
+ out = CArray.float64(@k) # ignored by the kernel's n<2 guards)
258
+ grouped.send(:__reduceat_variance__, @offsets, means, cnt, out)
259
+ out
260
+ end
261
+
262
+ # @overload stddev
263
+ # Returns per-segment SAMPLE standard deviation (ddof=1) as float64.
264
+ # Matches `CArray#stddev` per segment (fewer than two present values
265
+ # MASKED).
266
+ # @return [CArray]
267
+ def stddev(axis: nil)
268
+ return axis_by_masked_copy(axis, :stddev) if axis
269
+ m = moments
270
+ return per_segment(core_reduce_type(:stddev)) { |s| s.stddev } unless m
271
+ variance.sqrt # sqrt propagates the n<2 mask
272
+ end
273
+
274
+ # @overload prod
275
+ # Returns per-segment products as float64 (matching `CArray#prod`). An
276
+ # empty / fully-masked segment is `1.0` (the multiplicative identity).
277
+ # Single-pass reduceat for numeric values; per-segment fallback otherwise.
278
+ # @return [CArray]
279
+ def prod(axis: nil)
280
+ return axis_prod(axis) if axis
281
+ return per_segment(core_reduce_type(:prod)) { |s| s.prod } unless MONOID_TYPES.include?(grouped.data_type)
282
+ out = CArray.float64(@k)
283
+ grouped.send(:__reduceat_prod__, @offsets, out)
284
+ out
285
+ end
286
+
287
+ # @overload all
288
+ # Returns the per-segment `all` as boolean (matching `CArray#all`): true
289
+ # iff every present value is truthy (empty segment -> true, vacuously).
290
+ # The value data type must be boolean, as for `CArray#all`.
291
+ # @return [CArray]
292
+ def all
293
+ aa = all_any
294
+ aa ? aa[:all] : per_segment(CA_BOOLEAN) { |s| s.all }
295
+ end
296
+
297
+ # @overload any
298
+ # Returns the per-segment `any` as boolean (matching `CArray#any`): true
299
+ # iff some present value is truthy (empty segment -> false). The value
300
+ # data type must be boolean, as for `CArray#any`.
301
+ # @return [CArray]
302
+ def any
303
+ aa = all_any
304
+ aa ? aa[:any] : per_segment(CA_BOOLEAN) { |s| s.any }
305
+ end
306
+
307
+ # ---- tier 2 (fused / population / position) ------------------------------
308
+
309
+ # @overload minmax
310
+ # Returns the per-segment `[min, max]` pair (each a length-k CArray in the
311
+ # value data type; empty segments MASKED), matching `CArray#minmax`. Both come
312
+ # from one moments pass.
313
+ # @return [Array<CArray>]
314
+ def minmax(axis: nil)
315
+ if axis
316
+ # take both off one pass rather than asking min and max separately,
317
+ # which would run the kernel twice now that nothing is kept between
318
+ # calls -- this is the "keep the result" the axis: family expects
319
+ m = axis_moments(axis, :minmax)
320
+ return [m[:min], m[:max]]
321
+ end
322
+ [min, max]
323
+ end
324
+
325
+ # @overload variancep
326
+ # Per-segment POPULATION variance (ddof=0) as float64, matching
327
+ # `CArray#variancep`: empty / all-masked -> MASKED, single value -> 0.0.
328
+ # Derived from the sample variance (variancep = variance * (n-1) / n), so it
329
+ # reuses the centred two-pass kernel with no extra walk.
330
+ # @return [CArray]
331
+ def variancep(axis: nil)
332
+ return axis_by_masked_copy(axis, :variancep) if axis
333
+ m = moments
334
+ return per_segment(core_reduce_type(:variancep)) { |s| s.variancep } unless m
335
+ cnt = m[:count]
336
+ vp = variance * (cnt - 1).float64 / cnt.float64
337
+ vp[cnt.eq(1)] = 0.0 # one value: defined, unlike variance
338
+ vp
339
+ end
340
+
341
+ # @overload stddevp
342
+ # Per-segment POPULATION standard deviation (ddof=0) as float64.
343
+ # @return [CArray]
344
+ def stddevp(axis: nil)
345
+ return axis_by_masked_copy(axis, :stddevp) if axis
346
+ m = moments
347
+ return per_segment(core_reduce_type(:stddevp)) { |s| s.stddevp } unless m
348
+ variancep.sqrt
349
+ end
350
+
351
+ # @overload min_index
352
+ # Per-segment index of the minimum — the position within the
353
+ # segment's members (source order) — matching `CArray#min_index` per segment.
354
+ # Empty / all-masked segments are MASKED. Single-pass fused reduceat for
355
+ # numeric values; per-segment fallback otherwise.
356
+ # @return [CArray] length-k int64
357
+ def min_index
358
+ am = arg_minmax
359
+ am ? am[:min].copy : per_segment(CA_INT64) { |s| s.min_index }
360
+ end
361
+
362
+ # @overload max_index
363
+ # Per-segment index of the maximum. See {#min_index}.
364
+ # @return [CArray] length-k int64
365
+ def max_index
366
+ am = arg_minmax
367
+ am ? am[:max].copy : per_segment(CA_INT64) { |s| s.max_index }
368
+ end
369
+
370
+ # @overload min_addr
371
+ # Per-segment flat source address of the minimum — which cell of the source
372
+ # value holds it, matching `CArray#min_addr` per segment. Unlike {#min_index}
373
+ # (the position within the segment) this indexes back into the original array
374
+ # (`value.reshape(value.elements)[grp.min_addr]`). Empty segments MASKED.
375
+ # @return [CArray] length-k int64
376
+ def min_addr
377
+ segment_addr(min_index)
378
+ end
379
+
380
+ # @overload max_addr
381
+ # Per-segment flat source address of the maximum. See {#min_addr}.
382
+ # @return [CArray] length-k int64
383
+ def max_addr
384
+ segment_addr(max_index)
385
+ end
386
+
387
+ # @overload sort_addr
388
+ # Per-segment sort by flat source address. Returns a length-nvalid
389
+ # (= `elements.sum`) int64 CArray of the flat SOURCE addresses that sort each
390
+ # segment's members, segment by segment: part `c` holds segment `c`'s
391
+ # source addresses in ascending-value order, parts concatenated in
392
+ # order. So `value.reshape(value.elements)[grp.sort_addr]` yields
393
+ # the values segment by segment, sorted within each, and splitting by the
394
+ # {#elements} prefix sum gives per-segment. Excluded cells (in no segment) are
395
+ # omitted. A masked value sorts to the tail of its segment (as `CArray#sort`
396
+ # sends masked cells to the end), so with a mask the first address is the
397
+ # minimum but the last is the masked cell, not the maximum.
398
+ #
399
+ # Unlike {#min_index} / {#max_index} (positions within a segment), this
400
+ # indexes back into the original array; only the source-address form is
401
+ # offered, mirroring {#min_addr}.
402
+ # @return [CArray] length-nvalid int64
403
+ def sort_addr
404
+ out = CArray.int64(grouped.elements)
405
+ @k.times do |c|
406
+ lo, hi = @bounds[c], @bounds[c + 1]
407
+ next unless hi > lo
408
+ # View-local sort order of the segment (0..size-1), lifted to grouped
409
+ # slots, then mapped back to source addresses via perm.
410
+ out[lo...hi] = perm[grouped[lo...hi].sort_addr + lo]
411
+ end
412
+ out
413
+ end
414
+
415
+ # @overload wsum(weights)
416
+ # Per-segment weighted sum as float64, matching `CArray#wsum`. `weights` is
417
+ # a per-cell weight CArray in the source order (same elements as the value).
418
+ # Empty / all-masked segment -> 0.0 (the additive identity). A cell is
419
+ # skipped iff its value OR its weight is masked (core's contract).
420
+ # @param weights [CArray]
421
+ # @return [CArray]
422
+ def wsum (weights, axis: nil)
423
+ return axis_wsum_wmean(weights, axis, :wsum)[0] if axis
424
+ kernel = MONOID_TYPES.include?(grouped.data_type)
425
+ wg = scatter_weights(weights, kernel)
426
+ return kernel_weighted(wg)[0] if kernel
427
+ fold_weighted(:wsum, wg, 0.0) { |v, ws| v.wsum(ws) }
428
+ end
429
+
430
+ # @overload wmean(weights)
431
+ # Per-segment weighted mean as float64, matching `CArray#wmean`. Empty
432
+ # segment -> MASKED; a present segment whose weights sum to zero -> NaN
433
+ # (core's 0/0 contract).
434
+ # @param weights [CArray]
435
+ # @return [CArray]
436
+ def wmean (weights, axis: nil)
437
+ return axis_wsum_wmean(weights, axis, :wmean)[1] if axis
438
+ kernel = MONOID_TYPES.include?(grouped.data_type)
439
+ wg = scatter_weights(weights, kernel)
440
+ return kernel_weighted(wg)[1] if kernel
441
+ fold_weighted(:wmean, wg, UNDEF) { |v, ws| v.wmean(ws) }
442
+ end
443
+
444
+ # @overload reduce { |members| ... }
445
+ # Custom per-segment reduction (the escape hatch for statistics not in the
446
+ # named surface), mirroring `CArray#reduce_slab`. The block receives each
447
+ # segment's members (a CArray) and returns one value per segment.
448
+ # @yieldparam members [CArray]
449
+ # @return [CArray] length-k
450
+ # @overload reduce(init) { |acc, elem| ... }
451
+ # Per-segment fold: each segment's members are folded element by
452
+ # element starting from `init`.
453
+ # @param init [Object] initial accumulator.
454
+ # @return [CArray] length-k
455
+ def reduce (*args, data_type: nil, &blk)
456
+ raise LocalJumpError, "no block given (yield)" unless blk
457
+ dt = data_type || CA_OBJECT
458
+ if args.empty?
459
+ per_segment(dt) { |s| blk.call(s) }
460
+ else
461
+ init = args[0]
462
+ per_segment(dt) { |s|
463
+ acc = init
464
+ s.each { |e| acc = blk.call(acc, e) }
465
+ acc
466
+ }
467
+ end
468
+ end
469
+
470
+ # @overload map(data_type: nil) { |members| ... }
471
+ # Segment-wise element-wise transform, mirroring `CArray#map_slab`. The block
472
+ # receives each segment's members and returns either a same-length CArray
473
+ # (scattered back cell for cell) or a scalar (broadcast over the segment's
474
+ # cells). Returns a NEW CArray shaped like the source `value`; the original
475
+ # is not modified (`value[] = grp.map { ... }` for in-place). Excluded cells
476
+ # (in no segment) are UNDEF in the result.
477
+ #
478
+ # The result has the value's data type, or `data_type:` when it is given.
479
+ # A Face value gives a result of the same Face; a read-only Face (a
480
+ # categorical, a const string), which cannot be written cell by cell,
481
+ # gives an object array of its surface values.
482
+ #
483
+ # `axis:` is for a categorical group-by whose categorical classifies the
484
+ # positions along that axis: the block receives each category's positions
485
+ # along it, every other axis kept. A segment iterator has no axis: form.
486
+ # @yieldparam members [CArray]
487
+ # @return [CArray] shaped like the source value
488
+ def map (data_type: nil, axis: nil, &block)
489
+ raise LocalJumpError, "no block given (yield)" unless block
490
+ return axis_map(axis, data_type, &block) if axis
491
+ # Apply the block per segment, assembled in grouped (segment-contiguous)
492
+ # order: a same-length result scatters cell for cell, a scalar broadcasts.
493
+ scatter_segments(->(shape) { blank_output(shape, data_type) }, &block)
494
+ end
495
+
496
+ # ---- segment scan: within-segment running statistics ------------------
497
+ #
498
+ # The per-element-emit siblings of the reductions: unlike a reduction (which
499
+ # collapses each segment to one value) a scan preserves the source shape,
500
+ # each cell holding its segment's running statistic up to and including that
501
+ # cell, in source (row-major) order. A segment is a partition (each cell is
502
+ # in at most one segment), so the running value is single-valued.
503
+ # A cell in no segment is UNDEF. A masked cell inside a segment holds the
504
+ # running value, as `CArray#cumsum` does.
505
+ #
506
+ # Each answers as the core scan of the same name does over the segment: the
507
+ # data type is the core's (`cumsum` of a boolean is uint64, of an integer
508
+ # float64), a Face keeps its Face (`cummin` of a time column is a time), and
509
+ # a scan the core refuses (`cumsum` of a time column) raises in the core's
510
+ # words. When the fused C kernel __axis_group_scan__ answers in that data
511
+ # type, it runs with the whole source as a single grouped axis and the
512
+ # segment of each cell (#codes) as the single bundle, emitting in source
513
+ # order; otherwise each segment is scanned by the core and scattered back.
514
+
515
+ # @!method cumsum
516
+ # Per-segment inclusive running sum, source-shaped.
517
+ # @return [CArray]
518
+ # @!method cumprod
519
+ # Per-segment inclusive running product, source-shaped.
520
+ # @return [CArray]
521
+ # @!method cummax
522
+ # Per-segment inclusive running maximum, source-shaped.
523
+ # @return [CArray]
524
+ # @!method cummin
525
+ # Per-segment inclusive running minimum, source-shaped.
526
+ # @return [CArray]
527
+ # @!method cumcount
528
+ # Per-segment 1-based within-segment ordinal (int64), source-shaped.
529
+ # @return [CArray]
530
+ [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op|
531
+ define_method(op) { |axis: nil| axis ? axis_scan(op, axis) : scan(op) }
532
+ end
533
+
534
+ private
535
+
536
+ # The contiguous copy every reduction works from. On a CACategoricalIterator
537
+ # it does not exist when the classifier does not line up cell-for-cell with the value;
538
+ # such an iterator answers the per-fiber form only, and says so here rather
539
+ # than letting a nil surface as whatever NoMethodError it reaches first.
540
+ def grouped
541
+ @grouped || raise(ArgumentError, @no_flat)
542
+ end
543
+
544
+ # Per-segment cell counts, alongside #grouped and unavailable for the same
545
+ # reason.
546
+ def counts
547
+ @elements || raise(ArgumentError, @no_flat)
548
+ end
549
+
550
+ # A segment scan. When the axis-group scan kernel answers in the core's data
551
+ # type, it runs with the whole value as one grouped axis and the flat codes
552
+ # as the single bundle; it emits in source order, so the flat result
553
+ # reshapes straight back to the source shape. Otherwise each segment is
554
+ # scanned by the core and scattered back.
555
+ #
556
+ # With no segment at all the kernel has no group to size, so it is given
557
+ # one that no cell belongs to: every cell comes back UNDEF, as it does when
558
+ # there are segments and a cell is in none of them.
559
+ def scan (op)
560
+ CArray.__answer_as__(op) { scan_answered(op) }
561
+ end
562
+
563
+ def scan_answered (op)
564
+ # An empty Face has no cell to ask the core about, and nothing to scan.
565
+ return blank_output(@src_shape) if @value.face? && @value.elements.zero?
566
+ answer = core_scan_answer(op)
567
+ unless kernel_scans?(op, answer)
568
+ return scatter_segments(->(shape) { blank_like(answer, shape) }) { |s| s.public_send(op) }
569
+ end
570
+ bundle = @k > 0 ? [codes, @k, [0]]
571
+ : [CArray.int64(@value.elements).fill(-1), 1, [0]]
572
+ scan_source.reshape(@value.elements)
573
+ .__axis_group_scan__([0], [bundle], op)
574
+ .reshape(*@src_shape)
575
+ end
576
+
577
+ # The core's scan of one cell of the value: the scan answers in its data
578
+ # type and Face. A scan the core refuses raises here, in the core's words.
579
+ def core_scan_answer (op)
580
+ (@core_scan_answer ||= {})[op] ||= core_probe.public_send(op)
581
+ end
582
+
583
+ # Whether the axis-group scan kernel gives the core's answer. It reads
584
+ # an integer, float, boolean or object payload (not a complex, not a Face),
585
+ # and answers cumsum / cumprod in float64 (an
586
+ # object stays an object), cummax / cummin in the value's data type and
587
+ # cumcount in int64 -- which is not the core's answer for every data type
588
+ # (the core counts a boolean's running sum in uint64).
589
+ def kernel_scans? (op, answer)
590
+ return false if @value.face? || answer.face? || @value.complex?
591
+ kernel_type =
592
+ case op
593
+ when :cumcount then CA_INT64
594
+ when :cumsum, :cumprod then @value.data_type == CA_OBJECT ? CA_OBJECT : CA_FLOAT64
595
+ else @value.data_type
596
+ end
597
+ answer.data_type == kernel_type
598
+ end
599
+
600
+ # Applies the block to each segment's members in grouped order and scatters
601
+ # the answers back to the source cells; a cell in no segment is UNDEF.
602
+ # +blank+ builds an empty array of a given shape that can hold the answers.
603
+ def scatter_segments (blank)
604
+ out = blank.call(@src_shape)
605
+ out[] = UNDEF
606
+ return out if grouped.elements.zero?
607
+ transformed = blank.call([grouped.elements])
608
+ @k.times do |c|
609
+ lo, hi = @bounds[c], @bounds[c + 1]
610
+ transformed[lo...hi] = surface_for(transformed, yield(grouped[lo...hi])) if hi > lo
611
+ end
612
+ out.reshape(@value.elements)[perm] = transformed
613
+ out
614
+ end
615
+
616
+ # A blank array for #map: of +data_type+ when one is given, otherwise of the
617
+ # same kind as the value.
618
+ def blank_output (shape, data_type = nil)
619
+ data_type ? CArray.new(data_type, shape) : blank_like(@value, shape)
620
+ end
621
+
622
+ # An answer as +out+ can hold it. A Face goes into an object array as its
623
+ # surface values; stored as it is, the object array would receive its
624
+ # storage bytes.
625
+ def surface_for (out, answer)
626
+ return answer unless out.data_type == CA_OBJECT && answer.is_a?(CArray) && answer.face?
627
+ answer.to_a
628
+ end
629
+
630
+ # A blank array of +shape+ of the same kind as +model+: its data type, or its
631
+ # Face. A read-only Face cannot be written cell by cell, so its values are
632
+ # held as the surface objects they are.
633
+ def blank_like (model, shape)
634
+ if model.face? && model.read_only?
635
+ CArray.new(CA_OBJECT, shape)
636
+ else
637
+ model.__send__(:__blank_like__, shape)
638
+ end
639
+ end
640
+
641
+ # The values as they were when the iterator was built, in source order.
642
+ # Every no-axis reduction works from the contiguous copy taken then; the
643
+ # scans read @value, so a write through the source between two calls used to
644
+ # be visible to a cumsum and not to a sum, off one iterator.
645
+ #
646
+ # Rebuilt rather than copied a second time: @perm is exactly the classified
647
+ # cells and @grouped holds their values and their masks, which is everything
648
+ # a scan reads -- a cell classified by nothing is skipped on its code, before
649
+ # its value is looked at. So this costs nothing until a scan asks for it, and
650
+ # nothing at all for an iterator that never scans.
651
+ def scan_source
652
+ @scan_source ||=
653
+ begin
654
+ snap = @value.template
655
+ snap.reshape(snap.elements)[perm] = grouped
656
+ snap
657
+ end
658
+ end
659
+
660
+
661
+ # Permutation: perm[slot] = the source index whose value sits at that grouped
662
+ # slot, read by #map / #sort_addr / the *_addr reductions. A segment's copy
663
+ # is the source range itself, so this is a run of consecutive indices, built
664
+ # the first time it is asked for. (A CACategoricalIterator sets it at
665
+ # construction: the valid prefix of the categorical's cached sort_addr.)
666
+ def perm
667
+ @perm ||= CArray.int64(grouped.elements).seq!(@origin)
668
+ end
669
+
670
+ # The segment each source cell belongs to, -1 for a cell outside every
671
+ # segment. Read by #map, the scans and the weighted reductions; built the
672
+ # first time one of them asks, since the reductions do not need it.
673
+ def codes
674
+ @codes ||= begin
675
+ c = CArray.int64(@value.elements).fill(-1)
676
+ if grouped.elements > 0
677
+ c[@origin...(@origin + grouped.elements)] =
678
+ CArray.segment_index(offsets: @bounds)
679
+ end
680
+ c
681
+ end
682
+ end
683
+
684
+ # Lay a per-cell weight array out in segment-contiguous order (same layout as
685
+ # @grouped), so wsum / wmean can pair each segment's values with its weights.
686
+ # Weights are coerced to float64; the same counting-sort scatter propagates
687
+ # the weight mask and skips excluded cells, so wg lines up with @grouped.
688
+ # The weighted kernel reads float64 weights; the per-segment fallback hands
689
+ # them to the core as they are, so an object weight stays exact.
690
+ def scatter_weights (weights, as_float64 = true)
691
+ unless weights.elements == codes.elements
692
+ raise ArgumentError,
693
+ "wsum/wmean: weights.elements (#{weights.elements}) != " \
694
+ "value.elements (#{codes.elements})"
695
+ end
696
+ wf = as_float64 ? weights.float64 : weights
697
+ wg = CArray.new(wf.data_type, [grouped.elements], bytes: wf.bytes)
698
+ codes.send(:__categorical_scatter__, wf.reshape(wf.elements),
699
+ @offsets.copy, wg, @k)
700
+ wg
701
+ end
702
+
703
+ # Map a per-segment index to the flat source address via the
704
+ # permutation (grouped slot -> source index). The min/max sits at grouped slot
705
+ # offsets[c] + local_index[c]; perm carries it back to the source. Empty
706
+ # segments (masked local index) stay masked.
707
+ def segment_addr (local_index)
708
+ out = CArray.int64(@k)
709
+ @k.times do |c|
710
+ out[c] = local_index.is_masked[c] ? UNDEF
711
+ : perm[@offsets[c] + local_index[c]]
712
+ end
713
+ out
714
+ end
715
+
716
+ # Fused per-segment weighted sum + weighted mean (one C pass over the grouped
717
+ # copy, weights in segment order). Returns [wsum, wmean]; wmean is masked where a
718
+ # segment has no present (value AND weight) pair. Numeric value data types only.
719
+ def kernel_weighted (wg)
720
+ ws = CArray.float64(@k)
721
+ wm = CArray.float64(@k)
722
+ grouped.send(:__reduceat_wsum_wmean__, @offsets, wg, ws, wm)
723
+ [ws, wm]
724
+ end
725
+
726
+ # Per-segment weighted fallback for the value data types the kernel does not
727
+ # read (object): delegate each segment to CArray#wsum / #wmean, answering in
728
+ # the data type the core answers in. Empty segments take the given identity.
729
+ def fold_weighted (op, wg, empty)
730
+ out = CArray.new(core_reduce_type(op, CA_FLOAT64([[1.0]])), [@k])
731
+ @k.times do |c|
732
+ lo, hi = @bounds[c], @bounds[c + 1]
733
+ out[c] = hi > lo ? yield(grouped[lo...hi], wg[lo...hi]) : empty
734
+ end
735
+ out
736
+ end
737
+
738
+ # The members of segment `c` as a CArray slice of the grouped copy. An empty
739
+ # segment (zero-width segment) yields the shared empty array — a zero-length
740
+ # slice cannot be taken directly, and an empty array carries the same reduction
741
+ # contract we want (identity for sum, UNDEF for ratios).
742
+ def segment_slice (c)
743
+ lo, hi = @bounds[c], @bounds[c + 1]
744
+ hi > lo ? grouped[lo...hi] : @empty
745
+ end
746
+
747
+ # Single-pass reduceat moments (count / sum / min / max per segment), computed
748
+ # once over the grouped copy and cached — the whole point of the eager copy is
749
+ # that one scatter is followed by cheap single-pass reductions with no
750
+ # per-segment views. Nil for a non-numeric value data type (complex / object /
751
+ # bool), where the monoid reductions fall back to per_segment.
752
+ # numeric value data types the C moments kernel handles (int8..float64); bool /
753
+ # complex / object fall back to per_segment.
754
+ MONOID_TYPES = %i[int8 uint8 int16 uint16 int32 uint32
755
+ int64 uint64 float32 float64].freeze
756
+
757
+ def moments
758
+ return @moments if defined?(@moments)
759
+ @moments =
760
+ if MONOID_TYPES.include?(grouped.data_type)
761
+ dt = grouped.data_type
762
+ counts = CArray.int64(@k)
763
+ sums = CArray.float64(@k)
764
+ mins = CArray.new(dt, [@k])
765
+ maxs = CArray.new(dt, [@k])
766
+ grouped.send(:__reduceat_moments__, @offsets, counts, sums, mins, maxs)
767
+ { count: counts, sum: sums, min: mins, max: maxs }
768
+ end
769
+ end
770
+
771
+ # Single-pass fused per-segment argmin / argmax (min_index / max_index),
772
+ # cached. Nil for a non-numeric value data type (fall back to per_segment).
773
+ def arg_minmax
774
+ return @arg_minmax if defined?(@arg_minmax)
775
+ @arg_minmax =
776
+ if MONOID_TYPES.include?(grouped.data_type)
777
+ mn = CArray.int64(@k)
778
+ mx = CArray.int64(@k)
779
+ grouped.send(:__reduceat_argminmax__, @offsets, mn, mx)
780
+ { min: mn, max: mx }
781
+ end
782
+ end
783
+
784
+ # Single-pass fused per-segment boolean all / any, cached. Nil unless the
785
+ # value data type is boolean (fall back to per_segment, which raises like
786
+ # CArray#all on a non-boolean).
787
+ def all_any
788
+ return @all_any if defined?(@all_any)
789
+ @all_any =
790
+ if grouped.data_type == CA_BOOLEAN
791
+ a = CArray.boolean(@k)
792
+ o = CArray.boolean(@k)
793
+ grouped.send(:__reduceat_all_any__, @offsets, a, o)
794
+ { all: a, any: o }
795
+ end
796
+ end
797
+
798
+ # The data type the core reduction `op` promotes this value to. Asked of the
799
+ # core itself -- a one-cell reduction of the value's type -- rather than
800
+ # restated here, so a per-segment answer cannot drift from `CArray#<op>`
801
+ # (`sum` on an integer promotes, `accumulate` stays, `min` / `max` keep the
802
+ # type but a boolean widens, `prod` on an object stays an object). A payload
803
+ # the core refuses to fold this way raises here, with the core's own error.
804
+ # Probes the core with a one-cell array of the value's data type and takes
805
+ # the answer's. Asks @value rather than the grouped copy, which has the same
806
+ # data type but does not exist on an iterator that answers only the
807
+ # per-fiber form -- and `accumulate(axis:)`, the one axis: member that needs
808
+ # this, is exactly the case that would have found it missing.
809
+ def core_reduce_type (op, *args)
810
+ (@core_reduce_type ||= {})[[op, args]] ||=
811
+ @value.face? && @value.elements.zero? ?
812
+ CA_OBJECT : core_probe.public_send(op, *args, axis: 1).data_type
813
+ end
814
+
815
+ # A one-cell array of the same kind as the values, so the core answers about
816
+ # the same thing the segment slices will hand back. For a Face that is a view
817
+ # of the values themselves rather than a blank: a Face cannot be allocated
818
+ # from its surface data type alone, and a blank storage array is not a valid
819
+ # Face for every one of them -- a const string's record indexes a shared
820
+ # pool, so a zeroed record points nowhere.
821
+ def core_probe
822
+ return CArray.new(@value.data_type, [1, 1], bytes: @value.bytes) unless @value.face?
823
+ @value.reshape(@value.elements)[[0]].reshape(1, 1)
824
+ end
825
+
826
+ # Whether an output can be built by lifting one. A Face is filled by writing
827
+ # surface values into storage, so this needs a Face that can be written
828
+ # into; a read-only one -- a const string's records index a shared pool, a
829
+ # categorical's codes index a vocabulary -- has no blank form to fill.
830
+ def face_output?
831
+ @value.face? && ! @value.read_only?
832
+ end
833
+
834
+ # Build a length-k typed output by folding each segment's members with the
835
+ # given reduction block. Fallback path (order statistics, and monoids on a
836
+ # non-numeric value data type): each segment is delegated to the same CArray
837
+ # reduction, so the per-segment result matches `CArray#<reduction>` over that
838
+ # segment's members — the mask carries the "insufficient present data" contract
839
+ # for free (an all-masked segment reduces like an empty one; identity-bearing
840
+ # reductions return their identity, ratios return UNDEF).
841
+ def per_segment (data_type)
842
+ out = if ! @value.face? || data_type != @value.data_type
843
+ bytes = (data_type == @value.data_type) ? @value.bytes : 0
844
+ CArray.new(data_type, [@k], bytes: bytes)
845
+ elsif face_output?
846
+ # the core answered in the values' own Face, so the output is one
847
+ # too: CATime#min hands back a CATime::Element, which only a
848
+ # CATime has anywhere to put
849
+ @value.__send__(:__blank_like__, [@k])
850
+ else
851
+ # a read-only Face cannot be filled, so its answers are collected
852
+ # as the surface objects they already are
853
+ CArray.new(CA_OBJECT, [@k])
854
+ end
855
+ # A member the core refuses for this Face still refuses: the segment slice is
856
+ # the Face, so the refusal comes from there, in the core's own words.
857
+ @k.times { |c| out[c] = yield(segment_slice(c)) }
858
+ out
859
+ end
860
+
861
+ # ---- no axis: form -------------------------------------------------------
862
+
863
+ def axis_by_masked_copy (*)
864
+ raise NotImplementedError, "#{self.class} has no axis: form"
865
+ end
866
+
867
+ def axis_counts (*)
868
+ raise NotImplementedError, "#{self.class} has no axis: form"
869
+ end
870
+
871
+ def axis_mean (*)
872
+ raise NotImplementedError, "#{self.class} has no axis: form"
873
+ end
874
+
875
+ def axis_moments (*)
876
+ raise NotImplementedError, "#{self.class} has no axis: form"
877
+ end
878
+
879
+ def axis_order_stat (*)
880
+ raise NotImplementedError, "#{self.class} has no axis: form"
881
+ end
882
+
883
+ def axis_scan (*)
884
+ raise NotImplementedError, "#{self.class} has no axis: form"
885
+ end
886
+
887
+ def axis_map (*)
888
+ raise NotImplementedError, "#{self.class} has no axis: form"
889
+ end
890
+
891
+ def axis_prod (*)
892
+ raise NotImplementedError, "#{self.class} has no axis: form"
893
+ end
894
+
895
+ def axis_sum (*)
896
+ raise NotImplementedError, "#{self.class} has no axis: form"
897
+ end
898
+
899
+ def axis_wsum_wmean (*)
900
+ raise NotImplementedError, "#{self.class} has no axis: form"
901
+ end
902
+
903
+ # The present cells per segment a weighted reduction counts: those whose
904
+ # value and weight are both present, as CArray#wsum / #wmean count them.
905
+ def present_pair_count (weights, axis)
906
+ if axis
907
+ paired = @value.copy
908
+ paired[weights.is_masked] = UNDEF if weights.has_mask?
909
+ return paired.group_by_category(@cat).count_not_masked(axis: axis)
910
+ end
911
+ both = grouped.is_not_masked & scatter_weights(weights).is_not_masked
912
+ both.int32.segments(offsets: @bounds).sum # one pass, unlike count(true)
913
+ end
914
+
915
+ # min_count: and fill_value: on a value reduction, as a core reduction
916
+ # takes them: a segment with fewer than min_count present values is
917
+ # UNDEF, and fill_value then fills the UNDEF cells, those min_count made
918
+ # and those the reduction answers for an empty segment. Each reduction
919
+ # is left to answer as it does; this only masks and fills its result.
920
+ # A weighted reduction counts the cells whose value and weight are both
921
+ # present.
922
+ module ReductionCountAndFill
923
+ REDUCTIONS = %i[sum accumulate max min minmax mean median percentile
924
+ variance stddev variancep stddevp prod wsum wmean
925
+ min_index max_index min_addr max_addr
926
+ count count_not_masked count_masked].freeze
927
+
928
+ # A count of the mask reads every cell of the segment, as the core's does.
929
+ def self.mask_count? (op, args)
930
+ op == :count_not_masked || op == :count_masked ||
931
+ (op == :count && (args.empty? || args[0].equal?(UNDEF)))
932
+ end
933
+
934
+ REDUCTIONS.each do |op|
935
+ define_method(op) do |*args, min_count: nil, fill_value: nil, **kw|
936
+ # The segment reduction already leaves an empty segment as the core
937
+ # does, so only a min_count asking for more is applied here.
938
+ need = CArray.__min_count_need__(op, min_count, 0)
939
+ # an error from the core reductions a member is made of names the member
940
+ result = CArray.__answer_as__(op) { super(*args, **kw) }
941
+ return result if need == 0 && fill_value.nil?
942
+ if need > 0
943
+ present = if op == :wsum || op == :wmean
944
+ present_pair_count(args[0], kw[:axis])
945
+ elsif ReductionCountAndFill.mask_count?(op, args)
946
+ if kw[:axis]
947
+ raise NotImplementedError,
948
+ "#{self.class}##{op}(axis:, min_count:) is not implemented"
949
+ end
950
+ elements
951
+ else
952
+ count_not_masked(**kw)
953
+ end
954
+ short = present.lt(need)
955
+ end
956
+ if short
957
+ (result.is_a?(Array) ? result : [result]).each { |r| r[short] = UNDEF }
958
+ end
959
+ CArray.__reduce_fill__(result, fill_value)
960
+ end
961
+ end
962
+ end
963
+ prepend ReductionCountAndFill
964
+
965
+ end
966
+
967
+
968
+ class CArray
969
+ # @overload segments(offsets:)
970
+ # Returns a {CASegmentIterator} that reduces `self` segment by segment,
971
+ # segment `c` being the cells `offsets[c]...offsets[c + 1]` in flatten
972
+ # order. Cells outside `offsets[0]...offsets[-1]` belong to no segment.
973
+ #
974
+ # data.segments(offsets: [0, 2, 2, 5]).sum # one sum per segment
975
+ #
976
+ # @param offsets [CArray, Array<Integer>] `k + 1` non-decreasing
977
+ # boundaries within `0..elements`.
978
+ # @return [CASegmentIterator]
979
+ # @overload segments(lengths:)
980
+ # The same from the `k` segment lengths, the first segment starting at
981
+ # cell 0.
982
+ # @param lengths [CArray, Array<Integer>] one count per segment.
983
+ # @return [CASegmentIterator]
984
+ # @raise [ArgumentError] for decreasing or empty offsets, a negative or
985
+ # masked length, segments reaching outside `self`, or unless exactly one
986
+ # of `offsets:` and `lengths:` is given.
987
+ def segments (offsets: nil, lengths: nil)
988
+ CASegmentIterator.new(self, offsets: offsets, lengths: lengths)
989
+ end
990
+ end