carray 2.0.1 → 3.0.0

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 (339) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -25
  3. data/CHANGELOG.md +16 -0
  4. data/LICENSE +1 -1
  5. data/NEWS.md +3 -0
  6. data/README.md +128 -44
  7. data/carray.gemspec +22 -24
  8. data/ext/ca_array_pool.c +91 -0
  9. data/ext/ca_axis_descriptor.h +186 -0
  10. data/ext/ca_axis_dispatch.c +924 -0
  11. data/ext/ca_axis_group.c +1208 -0
  12. data/ext/ca_bincmp_dispatch.c +76 -0
  13. data/ext/ca_bincmp_dispatch.h +85 -0
  14. data/ext/ca_binop_dispatch.c +125 -0
  15. data/ext/ca_binop_dispatch.h +159 -0
  16. data/ext/ca_categorical_iterator.c +1375 -0
  17. data/ext/ca_compare.c +94 -0
  18. data/ext/ca_compare.h +26 -0
  19. data/ext/ca_composite_dispatch.c +414 -0
  20. data/ext/ca_composite_dispatch.h +116 -0
  21. data/ext/ca_for_buffer.h +96 -0
  22. data/ext/ca_for_each_element.h +241 -0
  23. data/ext/ca_group_iter.c +304 -0
  24. data/ext/ca_iter_substrate.h +325 -0
  25. data/ext/ca_kernel_iterator.c +4321 -0
  26. data/ext/ca_kernel_iterator.h +2603 -0
  27. data/ext/ca_moncmp_dispatch.c +37 -0
  28. data/ext/ca_moncmp_dispatch.h +62 -0
  29. data/ext/ca_monop_dispatch.c +200 -0
  30. data/ext/ca_monop_dispatch.h +235 -0
  31. data/ext/ca_obj_array.c +355 -359
  32. data/ext/ca_obj_bincmp.c +809 -0
  33. data/ext/ca_obj_binop.c +892 -0
  34. data/ext/ca_obj_bitarray.c +369 -164
  35. data/ext/ca_obj_bitfield.c +294 -234
  36. data/ext/ca_obj_block.c +189 -711
  37. data/ext/ca_obj_byte_swap.c +766 -0
  38. data/ext/ca_obj_const_string.c +965 -0
  39. data/ext/ca_obj_face.c +670 -0
  40. data/ext/ca_obj_face.h +247 -0
  41. data/ext/ca_obj_fake.c +228 -100
  42. data/ext/ca_obj_farray.c +54 -441
  43. data/ext/ca_obj_field.c +82 -529
  44. data/ext/ca_obj_fixlen_string.c +306 -0
  45. data/ext/ca_obj_grid.c +858 -440
  46. data/ext/ca_obj_meld.c +1034 -0
  47. data/ext/ca_obj_moncmp.c +569 -0
  48. data/ext/ca_obj_monop.c +1111 -0
  49. data/ext/ca_obj_object.c +772 -297
  50. data/ext/ca_obj_record.c +468 -0
  51. data/ext/ca_obj_reduce.c +97 -82
  52. data/ext/ca_obj_refer.c +569 -459
  53. data/ext/ca_obj_remap.c +475 -0
  54. data/ext/ca_obj_repeat.c +92 -477
  55. data/ext/ca_obj_roll.c +616 -0
  56. data/ext/ca_obj_select.c +344 -296
  57. data/ext/ca_obj_select_axis.c +1296 -0
  58. data/ext/ca_obj_shift.c +229 -791
  59. data/ext/ca_obj_source.c +78 -0
  60. data/ext/ca_obj_stack.c +1173 -0
  61. data/ext/ca_obj_stride.c +2501 -0
  62. data/ext/ca_obj_string.c +268 -0
  63. data/ext/ca_obj_tile.c +614 -0
  64. data/ext/ca_obj_time.c +546 -0
  65. data/ext/ca_obj_timedelta.c +435 -0
  66. data/ext/ca_obj_transpose.c +62 -516
  67. data/ext/ca_obj_triop.c +746 -0
  68. data/ext/ca_obj_unbound_repeat.c +207 -240
  69. data/ext/ca_obj_window.c +1131 -563
  70. data/ext/ca_op_byte_swap.c +175 -0
  71. data/ext/ca_op_ipower.c +319 -0
  72. data/ext/ca_op_powi.h +88 -0
  73. data/ext/ca_sort_kernels.h +132 -0
  74. data/ext/ca_sweep_engine.c +430 -0
  75. data/ext/ca_sweep_engine.h +157 -0
  76. data/ext/ca_transform_common.c +228 -0
  77. data/ext/ca_triop_dispatch.c +55 -0
  78. data/ext/ca_triop_dispatch.h +62 -0
  79. data/ext/carray.h +795 -402
  80. data/ext/carray_access.c +831 -711
  81. data/ext/carray_attribute.c +98 -329
  82. data/ext/carray_bincount.c +255 -0
  83. data/ext/carray_broadcast.c +283 -0
  84. data/ext/carray_call_cfunc.c +1360 -828
  85. data/ext/carray_call_cfunc.h +160 -0
  86. data/ext/carray_cast.c +1212 -301
  87. data/ext/carray_cast_func.rb +81 -40
  88. data/ext/carray_class.c +53 -63
  89. data/ext/carray_config.h +28 -0
  90. data/ext/carray_conversion.c +350 -346
  91. data/ext/carray_copy.c +156 -268
  92. data/ext/carray_core.c +1341 -198
  93. data/ext/carray_count.c +312 -0
  94. data/ext/carray_data_type.c +43 -19
  95. data/ext/carray_element.c +585 -213
  96. data/ext/carray_factorize.c +2542 -0
  97. data/ext/carray_generate.c +230 -559
  98. data/ext/carray_histogram.c +490 -0
  99. data/ext/carray_hold.c +228 -0
  100. data/ext/carray_index_classifier.c +1035 -0
  101. data/ext/carray_index_classifier.h +27 -0
  102. data/ext/carray_internal.h +120 -0
  103. data/ext/carray_kernels_bincmp.c +4445 -0
  104. data/ext/carray_kernels_binop.c +10979 -0
  105. data/ext/carray_kernels_init.c +36 -0
  106. data/ext/carray_kernels_map.c +3466 -0
  107. data/ext/carray_kernels_moncmp.c +2096 -0
  108. data/ext/carray_kernels_monop.c +18312 -0
  109. data/ext/carray_kernels_reduce_aggregate.c +25836 -0
  110. data/ext/carray_kernels_reduce_boolean.c +329 -0
  111. data/ext/carray_kernels_reduce_cumulative.c +14592 -0
  112. data/ext/carray_kernels_reduce_extreme.c +16947 -0
  113. data/ext/carray_kernels_reduce_variance.c +3909 -0
  114. data/ext/carray_kernels_scan.c +3692 -0
  115. data/ext/carray_kernels_search.c +32137 -0
  116. data/ext/carray_kernels_sort.c +10625 -0
  117. data/ext/carray_kernels_triop.c +1391 -0
  118. data/ext/carray_lazy.c +567 -0
  119. data/ext/carray_loop.c +88 -200
  120. data/ext/carray_mask.c +848 -154
  121. data/ext/carray_math_kernel.h +120 -0
  122. data/ext/carray_mathfunc.c +10 -241
  123. data/ext/carray_median_percentile.c +1257 -0
  124. data/ext/carray_memory_view.c +1625 -0
  125. data/ext/carray_operator.c +1526 -318
  126. data/ext/carray_order.c +664 -1394
  127. data/ext/carray_partition.c +416 -0
  128. data/ext/carray_random.c +518 -0
  129. data/ext/carray_scatter.c +357 -0
  130. data/ext/carray_slab.c +1219 -0
  131. data/ext/carray_slab.h +84 -0
  132. data/ext/carray_sort.c +829 -0
  133. data/ext/carray_sort_kernel.c +620 -0
  134. data/ext/carray_struct.c +695 -0
  135. data/ext/carray_test.c +343 -229
  136. data/ext/carray_undef.c +34 -17
  137. data/ext/carray_utils.c +175 -74
  138. data/ext/extconf.rb +216 -55
  139. data/ext/mk_call_cfunc.rb +480 -0
  140. data/ext/mkkernel.rb +8842 -0
  141. data/ext/ruby_carray.c +202 -101
  142. data/ext/version.h +5 -15
  143. data/ext/version.rb +5 -13
  144. data/lib/carray/arrow_tensor.rb +401 -0
  145. data/lib/carray/attribute.rb +166 -0
  146. data/lib/carray/autoload_carray.rb +220 -0
  147. data/lib/carray/autoload_method_extension.rb +44 -0
  148. data/lib/carray/axis_group.rb +711 -0
  149. data/lib/carray/basics.rb +481 -0
  150. data/lib/carray/bincount_nd.rb +358 -0
  151. data/lib/carray/block_iterator.rb +604 -0
  152. data/lib/carray/boolean_reduce.rb +109 -0
  153. data/lib/carray/categorical.rb +561 -0
  154. data/lib/carray/categorical_iterator.rb +1062 -0
  155. data/lib/carray/complex.rb +150 -0
  156. data/lib/carray/conditional.rb +216 -0
  157. data/lib/carray/const_string.rb +228 -0
  158. data/lib/carray/construct.rb +139 -328
  159. data/lib/carray/core_extensions.rb +240 -0
  160. data/lib/carray/data_type_extension.rb +233 -0
  161. data/lib/carray/fixlen_string.rb +95 -0
  162. data/lib/carray/frame/concat.rb +132 -0
  163. data/lib/carray/frame/convert.rb +95 -0
  164. data/lib/carray/frame/csv_parser.rb +211 -0
  165. data/lib/carray/frame/frame.rb +649 -0
  166. data/lib/carray/frame/group.rb +186 -0
  167. data/lib/carray/frame/io.rb +164 -0
  168. data/lib/carray/frame/join.rb +248 -0
  169. data/lib/carray/frame/records.rb +99 -0
  170. data/lib/carray/frame/sort.rb +113 -0
  171. data/lib/carray/frame/verbs.rb +299 -0
  172. data/lib/carray/frame.rb +16 -0
  173. data/lib/carray/histogram.rb +512 -0
  174. data/lib/carray/inspect.rb +37 -20
  175. data/lib/carray/iterator.rb +57 -349
  176. data/lib/carray/lazy.rb +889 -0
  177. data/lib/carray/mask_gap_fill.rb +200 -0
  178. data/lib/carray/math.rb +78 -342
  179. data/lib/carray/meld_reduce.rb +289 -0
  180. data/lib/carray/methods/align_addr.rb +116 -0
  181. data/lib/carray/methods/bin.rb +128 -0
  182. data/lib/carray/methods/bincount.rb +87 -0
  183. data/lib/carray/methods/bit_string.rb +92 -0
  184. data/lib/carray/methods/broadcast.rb +63 -0
  185. data/lib/carray/methods/choose.rb +39 -0
  186. data/lib/carray/methods/composition.rb +280 -0
  187. data/lib/carray/methods/gather_nd.rb +206 -0
  188. data/lib/carray/methods/index.rb +39 -0
  189. data/lib/carray/methods/insert_block.rb +99 -0
  190. data/lib/carray/methods/is_in.rb +141 -0
  191. data/lib/carray/methods/join.rb +90 -0
  192. data/lib/carray/methods/locate_addr.rb +47 -0
  193. data/lib/carray/methods/mask_duplicates.rb +41 -0
  194. data/lib/carray/methods/meshgrid.rb +91 -0
  195. data/lib/carray/methods/mode.rb +126 -0
  196. data/lib/carray/methods/nunique.rb +46 -0
  197. data/lib/carray/methods/resize.rb +56 -0
  198. data/lib/carray/methods/snap.rb +156 -0
  199. data/lib/carray/methods/string_format.rb +57 -0
  200. data/lib/carray/methods/unique.rb +47 -0
  201. data/lib/carray/methods/value_counts.rb +71 -0
  202. data/lib/carray/mkmf.rb +124 -101
  203. data/lib/carray/runtime.rb +108 -0
  204. data/lib/carray/serialize.rb +478 -167
  205. data/lib/carray/slab_iterator.rb +292 -0
  206. data/lib/carray/stack.rb +291 -0
  207. data/lib/carray/string.rb +56 -180
  208. data/lib/carray/string_operation_extension.rb +289 -0
  209. data/lib/carray/struct.rb +335 -323
  210. data/lib/carray/struct_builder.rb +697 -0
  211. data/lib/carray/table.rb +41 -2
  212. data/lib/carray/time.rb +2255 -38
  213. data/lib/carray/window_iterator.rb +655 -0
  214. data/lib/carray.rb +55 -57
  215. metadata +163 -130
  216. data/Rakefile +0 -51
  217. data/TODO.md +0 -18
  218. data/ext/ca_iter_block.c +0 -257
  219. data/ext/ca_iter_dimension.c +0 -299
  220. data/ext/ca_iter_window.c +0 -214
  221. data/ext/ca_obj_mapping.c +0 -644
  222. data/ext/carray_iterator.c +0 -641
  223. data/ext/carray_math.rb +0 -850
  224. data/ext/carray_numeric.c +0 -259
  225. data/ext/carray_sort_addr.c +0 -254
  226. data/ext/carray_stat.c +0 -2100
  227. data/ext/carray_stat_proc.rb +0 -1999
  228. data/ext/mkmath.rb +0 -741
  229. data/ext/ruby_ccomplex.c +0 -509
  230. data/ext/ruby_float_func.c +0 -86
  231. data/lib/carray/array.rb +0 -8
  232. data/lib/carray/autoload/autoload_base.rb +0 -19
  233. data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
  234. data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
  235. data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
  236. data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
  237. data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
  238. data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
  239. data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
  240. data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
  241. data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
  242. data/lib/carray/autoload/autoload_gem_random.rb +0 -8
  243. data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
  244. data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
  245. data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
  246. data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
  247. data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
  248. data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
  249. data/lib/carray/autoload/autoload_object_link.rb +0 -1
  250. data/lib/carray/autoload/autoload_object_pack.rb +0 -2
  251. data/lib/carray/autoload.rb +0 -141
  252. data/lib/carray/basic.rb +0 -191
  253. data/lib/carray/broadcast.rb +0 -101
  254. data/lib/carray/compose.rb +0 -315
  255. data/lib/carray/convert.rb +0 -115
  256. data/lib/carray/info.rb +0 -110
  257. data/lib/carray/io/imagemagick.rb +0 -235
  258. data/lib/carray/mask.rb +0 -102
  259. data/lib/carray/math/histogram.rb +0 -177
  260. data/lib/carray/math/recurrence.rb +0 -93
  261. data/lib/carray/object/ca_obj_iterator.rb +0 -50
  262. data/lib/carray/object/ca_obj_link.rb +0 -50
  263. data/lib/carray/object/ca_obj_pack.rb +0 -99
  264. data/lib/carray/obsolete.rb +0 -256
  265. data/lib/carray/ordering.rb +0 -181
  266. data/lib/carray/testing.rb +0 -51
  267. data/lib/carray/transform.rb +0 -109
  268. data/mailmap +0 -1
  269. data/misc/Methods.ja.md +0 -182
  270. data/misc/NOTE +0 -51
  271. data/spec/Classes/CABitfield_spec.rb +0 -58
  272. data/spec/Classes/CABlockIterator_spec.rb +0 -114
  273. data/spec/Classes/CABlock_spec.rb +0 -205
  274. data/spec/Classes/CAField_spec.rb +0 -39
  275. data/spec/Classes/CAGrid_spec.rb +0 -75
  276. data/spec/Classes/CAMap_spec.rb +0 -0
  277. data/spec/Classes/CAMapping_spec.rb +0 -105
  278. data/spec/Classes/CAObject_attribute_spec.rb +0 -33
  279. data/spec/Classes/CAObject_spec.rb +0 -33
  280. data/spec/Classes/CARefer_spec.rb +0 -93
  281. data/spec/Classes/CARepeat_spec.rb +0 -65
  282. data/spec/Classes/CASelect_spec.rb +0 -22
  283. data/spec/Classes/CAShift_spec.rb +0 -16
  284. data/spec/Classes/CAStruct_spec.rb +0 -71
  285. data/spec/Classes/CATranspose_spec.rb +0 -60
  286. data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
  287. data/spec/Classes/CAWindow_spec.rb +0 -54
  288. data/spec/Classes/CAWrap_spec.rb +0 -8
  289. data/spec/Classes/CArray_spec.rb +0 -184
  290. data/spec/Classes/CScalar_spec.rb +0 -55
  291. data/spec/Features/feature_130_spec.rb +0 -19
  292. data/spec/Features/feature_attributes_spec.rb +0 -280
  293. data/spec/Features/feature_boolean_spec.rb +0 -98
  294. data/spec/Features/feature_broadcast.rb +0 -116
  295. data/spec/Features/feature_cast_function.rb +0 -19
  296. data/spec/Features/feature_cast_spec.rb +0 -33
  297. data/spec/Features/feature_class_spec.rb +0 -84
  298. data/spec/Features/feature_complex_spec.rb +0 -42
  299. data/spec/Features/feature_composite_spec.rb +0 -124
  300. data/spec/Features/feature_convert_spec.rb +0 -46
  301. data/spec/Features/feature_copy_spec.rb +0 -123
  302. data/spec/Features/feature_creation_spec.rb +0 -84
  303. data/spec/Features/feature_element_spec.rb +0 -144
  304. data/spec/Features/feature_extream_spec.rb +0 -54
  305. data/spec/Features/feature_generate_spec.rb +0 -74
  306. data/spec/Features/feature_index_spec.rb +0 -69
  307. data/spec/Features/feature_mask_spec.rb +0 -580
  308. data/spec/Features/feature_math_spec.rb +0 -97
  309. data/spec/Features/feature_order_spec.rb +0 -146
  310. data/spec/Features/feature_ref_store_spec.rb +0 -209
  311. data/spec/Features/feature_serialization_spec.rb +0 -125
  312. data/spec/Features/feature_stat_spec.rb +0 -397
  313. data/spec/Features/feature_virtual_spec.rb +0 -48
  314. data/spec/Features/method_eq_spec.rb +0 -81
  315. data/spec/Features/method_is_nan_spec.rb +0 -12
  316. data/spec/Features/method_map_spec.rb +0 -54
  317. data/spec/Features/method_max_with.rb +0 -20
  318. data/spec/Features/method_min_with.rb +0 -19
  319. data/spec/Features/method_ne_spec.rb +0 -18
  320. data/spec/Features/method_project_spec.rb +0 -188
  321. data/spec/Features/method_ref_spec.rb +0 -27
  322. data/spec/Features/method_round_spec.rb +0 -11
  323. data/spec/Features/method_s_linspace_spec.rb +0 -48
  324. data/spec/Features/method_s_span_spec.rb +0 -14
  325. data/spec/Features/method_seq_spec.rb +0 -47
  326. data/spec/Features/method_sort_with.rb +0 -43
  327. data/spec/Features/method_sorted_with.rb +0 -29
  328. data/spec/Features/method_span_spec.rb +0 -42
  329. data/spec/Features/method_wrap_readonly_spec.rb +0 -43
  330. data/spec/UnitTest/test_CAVirtual.rb +0 -214
  331. data/spec/spec_all.rb +0 -10
  332. data/utils/ca_ase.rb +0 -21
  333. data/utils/ca_methods.rb +0 -15
  334. data/utils/cast_checker.rb +0 -30
  335. data/utils/convert_test.rb +0 -73
  336. data/utils/extract_yard.rb +0 -22
  337. data/utils/guess_shape.rb +0 -76
  338. data/utils/monkey_patch_methods.rb +0 -62
  339. data/utils/remove_resource_fork.sh +0 -5
@@ -0,0 +1,711 @@
1
+ # Axis-group reduction surface. A grid array is classified BY ITS AXIS
2
+ # COORDINATES and reduced per group (grouped reduction over axis coordinates,
3
+ # with no scale-attached axis). This file holds the value-independent metadata
4
+ # layers; the apply hot path (`[]` type gate + CAGroupIterator + :group
5
+ # reduce driving) is C-level (ext/ca_group_iter.c, ext/carray_access.c).
6
+ #
7
+ # The per-axis classifier is CACategorical (carray/categorical.rb): dense
8
+ # integer codes (its storage parent) + a label vocabulary. A rank-N
9
+ # categorical (an N-D codes map, e.g. a [nlon,nlat] prefecture map) consumes N
10
+ # source axes and collapses them into ONE group axis. Construction is Ruby
11
+ # (`keys.categorize`); it is not the apply hot path.
12
+ #
13
+ # Two metadata layers built here:
14
+ #
15
+ # AxisGroup -- spec built by `value.axis_group(cat_or_nil, ...)`: slot
16
+ # position = axis, a CACategorical slot is a group axis,
17
+ # nil = band (held) axis. The value is a SHAPE TEMPLATE only
18
+ # (rank + axis lengths), value-independent so one spec serves
19
+ # many arrays.
20
+ # GroupLabels -- lazy, factorized label view returned by g.labels.
21
+
22
+ class CArray
23
+
24
+ # ------------------------------------------------------------------------
25
+ # CArray#axis_group(cat_or_nil, ...) -- build an AxisGroup spec.
26
+ #
27
+ # Slot position = source axis. A CACategorical slot consumes cat.ndim source
28
+ # axes (rank-1 = one axis, rank-N = several axes collapsed into one group
29
+ # axis); a nil slot is a band (held) axis. ALL axes must be given
30
+ # explicitly -- the rank-sum must equal self.ndim, trailing omission / nil
31
+ # fill is forbidden (explicit > implicit). The value is used as a
32
+ # shape TEMPLATE only (its data is never read).
33
+ def axis_group (*slots)
34
+ AxisGroup.new(self, slots)
35
+ end
36
+ end
37
+
38
+ # ----------------------------------------------------------------------------
39
+ # AxisGroup -- value-independent grouping spec.
40
+ # ----------------------------------------------------------------------------
41
+ class AxisGroup
42
+
43
+ # Built from CArray#axis_group. `value` is the shape template; `slots` is the
44
+ # raw slot list (CACategorical or nil per slot).
45
+ def initialize (value, slots)
46
+ ndim = value.ndim
47
+ shape = value.shape
48
+ meta = []
49
+ cursor = 0
50
+
51
+ slots.each do |slot|
52
+ case slot
53
+ when nil
54
+ if cursor >= ndim
55
+ raise IndexError,
56
+ "axis_group: too many slots for ndim #{ndim}"
57
+ end
58
+ meta << { kind: :band, axis: cursor, len: shape[cursor] }
59
+ cursor += 1
60
+ when CACategorical
61
+ rank = slot.ndim
62
+ if cursor + rank > ndim
63
+ raise IndexError,
64
+ "axis_group: categorical of rank #{rank} at axis #{cursor} " \
65
+ "exceeds ndim #{ndim}"
66
+ end
67
+ consumed = (cursor...cursor + rank).to_a
68
+ consumed.each_with_index do |a, j|
69
+ if slot.shape[j] != shape[a]
70
+ raise IndexError,
71
+ "axis_group: categorical axis #{j} length #{slot.shape[j]} " \
72
+ "!= source axis #{a} length #{shape[a]}"
73
+ end
74
+ end
75
+ meta << { kind: :group, axes: consumed, k: slot.labels.size,
76
+ codes: slot.codes, labels: slot.labels }
77
+ cursor += rank
78
+ else
79
+ raise TypeError,
80
+ "axis_group: slot must be a CACategorical or nil " \
81
+ "(got #{slot.class})"
82
+ end
83
+ end
84
+
85
+ unless cursor == ndim
86
+ raise IndexError,
87
+ "axis_group: slots cover #{cursor} of #{ndim} axes; all axes must " \
88
+ "be given explicitly (no trailing omission / implicit nil fill)"
89
+ end
90
+
91
+ @ndim = ndim
92
+ @template_shape = shape
93
+ @slot_meta = meta.freeze
94
+ freeze
95
+ end
96
+
97
+ attr_reader :ndim, :template_shape
98
+
99
+ # Number of output slots (= number of slots in the spec; each slot is one
100
+ # output axis before any band reduction).
101
+ def nslots
102
+ @slot_meta.size
103
+ end
104
+
105
+ # @!visibility private
106
+ def slot_meta
107
+ @slot_meta
108
+ end
109
+
110
+ # Build the reduction plan for the kernel. `fused` is the list of band SLOT
111
+ # positions to fold into the statistic (= integer axes given alongside
112
+ # :group). Returns
113
+ # [group_axes, bundles, group_dims, perm, squeeze]
114
+ # where
115
+ # group_axes : ascending source axes handed to the kernel as the slab.
116
+ # bundles : [codes, k, consumed_axes] per effective group slot (slot
117
+ # order); a fused band slot becomes a k=1 all-zero bundle.
118
+ # group_dims : the effective group dims (slot order) to reshape the leading
119
+ # K_total axis into.
120
+ # perm : permutation mapping the reshaped layout
121
+ # [*group_dims, *preserved_band_dims] to slot order.
122
+ # squeeze : slot positions (slot order) that are length-1 (fused bands)
123
+ # to drop after the transpose.
124
+ def reduce_plan (fused)
125
+ fused = Array(fused)
126
+ fused.each do |s|
127
+ m = @slot_meta[s]
128
+ unless m && m[:kind] == :band
129
+ raise IndexError,
130
+ "axis_group reduce: axis #{s} is not a band (held) axis; " \
131
+ "use :group to fold group axes"
132
+ end
133
+ end
134
+
135
+ eff_group = []
136
+ preserved_band = []
137
+ @slot_meta.each_with_index do |m, s|
138
+ if m[:kind] == :group || fused.include?(s)
139
+ eff_group << s
140
+ else
141
+ preserved_band << s
142
+ end
143
+ end
144
+
145
+ bundles = eff_group.map do |s|
146
+ m = @slot_meta[s]
147
+ if m[:kind] == :group
148
+ [m[:codes], m[:k], m[:axes]]
149
+ else
150
+ [CArray.int32(m[:len]), 1, [m[:axis]]]
151
+ end
152
+ end
153
+
154
+ group_axes = eff_group.flat_map { |s|
155
+ m = @slot_meta[s]
156
+ m[:kind] == :group ? m[:axes] : [m[:axis]]
157
+ }.sort
158
+
159
+ group_dims = eff_group.map { |s|
160
+ m = @slot_meta[s]
161
+ m[:kind] == :group ? m[:k] : 1
162
+ }
163
+
164
+ gp = eff_group.size
165
+ reshaped_axis = {}
166
+ eff_group.each_with_index { |s, p| reshaped_axis[s] = p }
167
+ preserved_band.each_with_index { |s, q| reshaped_axis[s] = gp + q }
168
+ perm = (0...@slot_meta.size).map { |s| reshaped_axis[s] }
169
+
170
+ [group_axes, bundles, group_dims, perm, fused.sort]
171
+ end
172
+
173
+ # ------------------------------------------------------------------------
174
+ # labels -- coordinate labels in the SAME index space as a reduced result.
175
+ #
176
+ # g.labels(i, j, k) -> the block's label tuple (Array); group axis =
177
+ # its label, band axis = its integer index.
178
+ # g.labels(axis: SPEC) -> a GroupLabels view in the index space of
179
+ # value[g].reduce(axis: SPEC) (lockstep).
180
+ # g.labels -> a GroupLabels view of the full grouped space.
181
+ def labels (*idx, axis: nil)
182
+ unless idx.empty?
183
+ if axis
184
+ raise ArgumentError, "labels: pass either positional index or axis:"
185
+ end
186
+ return full_labels[*idx]
187
+ end
188
+ return full_labels unless axis
189
+
190
+ has_group, fused = AxisGroup.parse_axis(axis)
191
+ unless has_group
192
+ raise ArgumentError,
193
+ "labels(axis:) needs :group (labels track the grouped result)"
194
+ end
195
+ survivors = (0...@slot_meta.size).reject { |s| fused.include?(s) }
196
+ GroupLabels.new(survivors.map { |s| axis_vector(s) })
197
+ end
198
+
199
+ # Full-space label view (every slot survives, slot order).
200
+ def full_labels
201
+ GroupLabels.new((0...@slot_meta.size).map { |s| axis_vector(s) })
202
+ end
203
+
204
+ # Per-slot label vector descriptor for GroupLabels.
205
+ # group slot -> [:names, labels_array]
206
+ # band slot -> [:identity, len]
207
+ def axis_vector (s)
208
+ m = @slot_meta[s]
209
+ if m[:kind] == :group
210
+ [:names, m[:labels]]
211
+ else
212
+ [:identity, m[:len]]
213
+ end
214
+ end
215
+
216
+ # Parse an axis: spec into [has_group, fused_band_slot_positions].
217
+ def self.parse_axis (axis)
218
+ has_group = false
219
+ fused = []
220
+ Array(axis).each do |a|
221
+ if a == :group
222
+ has_group = true
223
+ elsif a.is_a?(Integer)
224
+ fused << a
225
+ else
226
+ raise TypeError,
227
+ "axis_group reduce: axis entry must be :group or Integer " \
228
+ "(got #{a.inspect})"
229
+ end
230
+ end
231
+ [has_group, fused]
232
+ end
233
+
234
+ # @return [String]
235
+ def inspect
236
+ kinds = @slot_meta.map { |m| m[:kind] == :group ? "g#{m[:k]}" : "band" }
237
+ "#<AxisGroup ndim=#{@ndim} slots=[#{kinds.join(', ')}]>"
238
+ end
239
+ end
240
+
241
+ # ----------------------------------------------------------------------------
242
+ # GroupLabels -- lazy, factorized label view.
243
+ #
244
+ # Stores one 1-D label vector per surviving output axis (group axis = its
245
+ # categorical labels; band axis = identity 0...len). Block label tuples are
246
+ # built on demand from the product -- the N-tuple table is NEVER materialised,
247
+ # so storage is linear in the number of axes, not the block count.
248
+ # ----------------------------------------------------------------------------
249
+ class GroupLabels
250
+
251
+ def initialize (vectors)
252
+ @vectors = vectors # Array of [:names, array] / [:identity, len]
253
+ @shape = vectors.map { |kind, data| kind == :names ? data.size : data }
254
+ .freeze
255
+ freeze
256
+ end
257
+
258
+ attr_reader :shape
259
+
260
+ # @return [Integer] the number of grouped axes.
261
+ def ndim
262
+ @vectors.size
263
+ end
264
+
265
+ # Block label tuple at the given index (length = ndim).
266
+ def [] (*idx)
267
+ if idx.size != @vectors.size
268
+ raise IndexError,
269
+ "GroupLabels: expected #{@vectors.size} indices, got #{idx.size}"
270
+ end
271
+ @vectors.each_with_index.map do |(kind, data), ax|
272
+ i = idx[ax]
273
+ len = (kind == :names) ? data.size : data
274
+ i += len if i < 0
275
+ if i < 0 || i >= len
276
+ raise IndexError, "GroupLabels: index #{idx[ax]} out of range for axis #{ax}"
277
+ end
278
+ (kind == :names) ? data[i] : i
279
+ end
280
+ end
281
+
282
+ # The label vector for output axis k (Array): group labels or the identity
283
+ # range of a band axis.
284
+ def axis (k)
285
+ kind, data = @vectors[k]
286
+ (kind == :names) ? data.dup : (0...data).to_a
287
+ end
288
+
289
+ # All per-axis label vectors (the table axis headings).
290
+ def coords
291
+ (0...@vectors.size).map { |k| axis(k) }
292
+ end
293
+
294
+ # @return [String]
295
+ def inspect
296
+ "#<GroupLabels shape=#{@shape.inspect}>"
297
+ end
298
+ end
299
+
300
+ # Let the C [] type gate (ext/ca_group_iter.c) recognise these classes without
301
+ # a kind_of on every index. Registered once on first load of this file, which
302
+ # happens either eagerly (a `axis_group` / AxisGroup call) or lazily from the C
303
+ # gate itself the first time it meets a fixlen-surface CArray index (see
304
+ # ca_argv_has_group). CACategorical is the classifier, so it must be defined
305
+ # before the register call.
306
+ # CAGroupIterator (C-defined in ext/ca_group_iter.c) — the reduction dispatcher
307
+ # returned by the `[]` group gate. Its scatterable reductions (sum / prod / mean
308
+ # / min / max / variance / stddev / variancep / stddevp / count /
309
+ # count_not_masked / all / any) bind in C to one driver; the rest of the common
310
+ # iterator surface that composes cheaply from those is added here.
311
+ class CAGroupIterator
312
+ # Per-group classified cell count (mask-independent) = count on the
313
+ # mask-stripped value, so every classified cell is counted regardless of the
314
+ # value mask (unlike count / count_not_masked, which count present cells).
315
+ def elements (**kw)
316
+ self.class.__build__(value.value, spec).count(**kw)
317
+ end
318
+
319
+ # Per-group count of value-masked cells = elements - count_not_masked.
320
+ def count_masked (**kw)
321
+ elements(**kw) - count_not_masked(**kw)
322
+ end
323
+
324
+ # Per-group [min, max] pair (matching CArray#minmax).
325
+ def minmax (**kw)
326
+ [min(**kw), max(**kw)]
327
+ end
328
+
329
+ # @overload min_index
330
+ # @overload max_index
331
+ # Not provided for a group iterator: a group preserves source order, so a
332
+ # within-group index is weak (the members are not laid out in a private
333
+ # axis to index into). Use `min_addr` / `max_addr` for the winner's flat
334
+ # source address, which indexes back into the original array. Raises
335
+ # NotImplementedError.
336
+ # @raise [NotImplementedError]
337
+ def min_index (*)
338
+ raise NotImplementedError,
339
+ "CAGroupIterator has no min_index: a group preserves source order, so " \
340
+ "a within-group index is weak; use min_addr for the winner's flat " \
341
+ "source address (it indexes back into the original array)."
342
+ end
343
+
344
+ def max_index (*)
345
+ raise NotImplementedError,
346
+ "CAGroupIterator has no max_index: a group preserves source order, so " \
347
+ "a within-group index is weak; use max_addr for the winner's flat " \
348
+ "source address (it indexes back into the original array)."
349
+ end
350
+
351
+ # Weighted sum: group-sum of value*weight. `weights` is a per-cell CArray in
352
+ # the source layout (same shape as value); the product carries the combined
353
+ # value|weight mask, so masked cells drop out. Empty group -> 0.0 (identity).
354
+ # No weighted kernel needed -- it is a plain group-sum of a derived array.
355
+ def wsum (weights, **kw)
356
+ self.class.__build__(value * weights, spec).sum(**kw)
357
+ end
358
+
359
+ # Weighted mean = Sum(v*w) / Sum(w), both over the combined present-set. The
360
+ # `value*0 + weights` denominator carries the same value|weight mask, so its
361
+ # group-sum is Sum(w) over exactly the cells value*weight used. An empty group
362
+ # (no present pair) is UNDEF (matching CArray#wmean); a present group whose
363
+ # weights sum to zero yields NaN/Inf (core's 0/0).
364
+ def wmean (weights, **kw)
365
+ prod = value * weights
366
+ num = self.class.__build__(prod, spec).sum(**kw)
367
+ den = self.class.__build__(value * 0 + weights, spec).sum(**kw)
368
+ cnt = self.class.__build__(prod, spec).count_not_masked(**kw)
369
+ out = num / den
370
+ out[cnt.eq(0)] = UNDEF # no present pair -> masked (empty group)
371
+ out
372
+ end
373
+
374
+ # ---- segment scan: within-group running statistics ---------------------
375
+ #
376
+ # The per-element-emit siblings of the scatter reductions. Unlike a reduction
377
+ # (which collapses the group axes), a scan preserves the source shape: each
378
+ # cell holds the running statistic of its group up to and including that cell,
379
+ # in row-major position order along the grouped axes (per band). Excluded /
380
+ # source-masked cells are UNDEF (they join no running total). All five route
381
+ # through the shared scan_op dispatcher, driving the fused C kernel
382
+ # __axis_group_scan__ (ext/ca_axis_group.c) with peak O(k) extra. A scan
383
+ # cannot fold a band into the statistic (that would collapse an axis), so it
384
+ # accepts only axis: :group; without :group each delegates to the value's
385
+ # same-named scan.
386
+
387
+ # @overload cumsum(axis: :group)
388
+ # Per-group inclusive running sum (float64), source-shaped. Without :group
389
+ # delegates to the plain value cumsum.
390
+ # @return [CArray]
391
+ def cumsum (**kw)
392
+ scan_op(:cumsum, kw)
393
+ end
394
+
395
+ # @overload cumprod(axis: :group)
396
+ # Per-group inclusive running product (float64), source-shaped. float64 like
397
+ # cumsum since the product grows. Without :group delegates to value cumprod.
398
+ # @return [CArray]
399
+ def cumprod (**kw)
400
+ scan_op(:cumprod, kw)
401
+ end
402
+
403
+ # @overload cummax(axis: :group)
404
+ # Per-group inclusive running maximum, source-shaped, in the source dtype
405
+ # (extrema do not grow magnitude, so the dtype is preserved). The first
406
+ # member of a group emits its own value. Without :group delegates to value
407
+ # cummax.
408
+ # @return [CArray]
409
+ def cummax (**kw)
410
+ scan_op(:cummax, kw)
411
+ end
412
+
413
+ # @overload cummin(axis: :group)
414
+ # Per-group inclusive running minimum, source-shaped, in the source dtype.
415
+ # Without :group delegates to value cummin.
416
+ # @return [CArray]
417
+ def cummin (**kw)
418
+ scan_op(:cummin, kw)
419
+ end
420
+
421
+ # @overload cumcount(axis: :group)
422
+ # Per-group 1-based within-group ordinal (int64), source-shaped: the first
423
+ # member of a group is 1, the next 2, ... (a running count of the group's
424
+ # members up to and including the cell). Without :group delegates to the
425
+ # plain value cumcount (also a 1-based cumulative count of present cells).
426
+ # @return [CArray]
427
+ def cumcount (**kw)
428
+ scan_op(:cumcount, kw)
429
+ end
430
+
431
+ # ---- materialize path: order statistics + generic iteration ------------
432
+ #
433
+ # These need every member of a group held together, so (unlike the scatter
434
+ # reductions) they materialize. Every grouping -- one or several group slots,
435
+ # rank-1 or rank-N categoricals, with or without band (held) axes -- routes
436
+ # through one composite categorical over the group block (composite_layout).
437
+ # Two shapes follow from whether band axes are present:
438
+ #
439
+ # - FLAT (no band axes): the whole value is grouped through the composite
440
+ # categorical in one pass; a length-K result reshapes to the group-slot k
441
+ # dims (slot order).
442
+ # - BAND (band axes present): the group block is materialised one band
443
+ # position at a time (peak = O(group-block size), never a whole-array
444
+ # grouped copy), each block's length-K result written into the group-slot
445
+ # subspace of a slot-order output.
446
+ #
447
+ # Folding a band INTO an order statistic (axis: [:group, k]) gathers a band
448
+ # axis and a group axis into one statistic -- a different operation -- and
449
+ # remains a follow-up.
450
+
451
+ # @overload median
452
+ # Per-group median (float64), any grouping. See {#percentile}.
453
+ # @return [CArray]
454
+ def median (**kw)
455
+ order_stat(:median, [], kw)
456
+ end
457
+
458
+ # @overload percentile(*pers)
459
+ # Per-group percentile(s) (float64), any grouping.
460
+ # @return [CArray, Array<CArray>]
461
+ def percentile (*pers, **kw)
462
+ order_stat(:percentile, pers, kw)
463
+ end
464
+
465
+ # @overload quantile
466
+ # Per-group five-number summary `[min, Q1, median, Q3, max]`, any grouping.
467
+ # @return [Array<CArray>]
468
+ def quantile (**kw)
469
+ order_stat(:quantile, [], kw)
470
+ end
471
+
472
+ # @overload sort_addr
473
+ # Per-group sorted flat source addresses (group-major). Any grouping (single
474
+ # or composite group slots, rank-1 or rank-N, flat or band-preserving) is
475
+ # supported; with no :group it is a plain value.sort_addr.
476
+ # @return [CArray]
477
+ def sort_addr (**kw)
478
+ has_group, _ = AxisGroup.parse_axis(kw[:axis])
479
+ return value.sort_addr unless has_group
480
+ ccat, _kd, gslots, bslots, gaxes = composite_layout
481
+ return value.group_by_category(ccat).sort_addr if bslots.empty?
482
+ composite_band_sort_addr(ccat, gslots, bslots, gaxes)
483
+ end
484
+
485
+ # @overload each { |members| ... }
486
+ # Yields each group's members (a CArray). A flat grouping yields per group
487
+ # (composite category); a band-preserving grouping yields per (band position,
488
+ # composite category), band-major then group order. Without a block returns
489
+ # an Enumerator.
490
+ def each (&block)
491
+ ccat, _kd, gslots, bslots, gaxes = composite_layout
492
+ return value.group_by_category(ccat).each(&block) if bslots.empty?
493
+ return to_enum(:each) unless block
494
+ each_band_block(ccat, gslots, bslots, gaxes) { |_vi, _oi, _co, gi| gi.each(&block) }
495
+ self
496
+ end
497
+
498
+ # @overload map(data_type: nil) { |members| ... }
499
+ # Group-wise element-wise transform back to a source-shaped array. Excluded
500
+ # cells (in no group) are UNDEF. Any grouping (single or composite, flat or
501
+ # band-preserving).
502
+ # @return [CArray]
503
+ def map (data_type: nil, &block)
504
+ raise LocalJumpError, "no block given (yield)" unless block
505
+ ccat, _kd, gslots, bslots, gaxes = composite_layout
506
+ return value.group_by_category(ccat).map(data_type: data_type, &block) if bslots.empty?
507
+ dt = data_type || value.data_type
508
+ out = CArray.new(dt, value.shape)
509
+ each_band_block(ccat, gslots, bslots, gaxes) do |val_idx, _oi, _co, gi|
510
+ out[*val_idx] = gi.map(data_type: dt, &block)
511
+ end
512
+ out
513
+ end
514
+
515
+ # @overload reduce { |members| ... }
516
+ # @overload reduce(init) { |acc, elem| ... }
517
+ # Custom per-group reduction (dual form). Output shape = slot order (each
518
+ # group slot -> its k, each band slot -> its length). Any grouping (single or
519
+ # composite, flat or band-preserving).
520
+ # @return [CArray]
521
+ def reduce (*args, data_type: nil, &block)
522
+ raise LocalJumpError, "no block given (yield)" unless block
523
+ ccat, kdims, gslots, bslots, gaxes = composite_layout
524
+ if bslots.empty?
525
+ return value.group_by_category(ccat).reduce(*args, data_type: data_type, &block)
526
+ .reshape(*kdims)
527
+ end
528
+ dt = data_type || CA_OBJECT
529
+ out_shape = spec.slot_meta.map { |m| m[:kind] == :group ? m[:k] : m[:len] }
530
+ out = CArray.new(dt, out_shape)
531
+ each_band_block(ccat, gslots, bslots, gaxes) do |_vi, out_idx, _co, gi|
532
+ out[*out_idx] = gi.reduce(*args, data_type: dt, &block).reshape(*kdims)
533
+ end
534
+ out
535
+ end
536
+
537
+ private
538
+
539
+ # Build the composite categorical over the group block (the sub-array spanning
540
+ # every group axis). Each group slot -- rank-1 or rank-N -- contributes its
541
+ # per-axis codes; the slot codes are broadcast into the group-block rank (size
542
+ # at the slot's own axes, 1 elsewhere) and combined into one composite code in
543
+ # slot order: code = sum_i code_i * (product of k of later slots). A cell
544
+ # excluded by ANY slot (masked code) is masked in the composite, so it joins no
545
+ # group (the mask propagates through the broadcast arithmetic). Returns
546
+ # [ccat, kdims, group_slots, band_slots, group_axes], where ccat is a rank-1
547
+ # CACategorical over the flattened group block (k = product of the slots' k,
548
+ # synthetic integer labels) and kdims is the per-group-slot k in slot order
549
+ # (the shape a length-K result reshapes to). This is the classification the C
550
+ # scatter kernel computes on the fly, materialised once so the order
551
+ # statistics / iterate / sort_addr can hold each group's members together.
552
+ def composite_layout
553
+ gslots = spec.slot_meta.select { |m| m[:kind] == :group }
554
+ bslots = spec.slot_meta.select { |m| m[:kind] == :band }
555
+ gaxes = gslots.flat_map { |m| m[:axes] }.sort
556
+ shape = value.shape
557
+ gb_rank = gaxes.size
558
+ kdims = gslots.map { |m| m[:k] }
559
+ place = Array.new(gslots.size, 1) # place value per slot
560
+ (gslots.size - 2).downto(0) { |i| place[i] = place[i + 1] * gslots[i + 1][:k] }
561
+ comp = nil
562
+ gslots.each_with_index do |m, i|
563
+ target = Array.new(gb_rank, 1)
564
+ m[:axes].each { |a| target[gaxes.index(a)] = shape[a] }
565
+ term = m[:codes].reshape(*target)
566
+ term = term * place[i] if place[i] != 1
567
+ comp = comp.nil? ? term : comp + term
568
+ end
569
+ gb_size = gaxes.map { |a| shape[a] }.inject(1, :*)
570
+ ccat = CACategorical.from_codes(comp.reshape(gb_size),
571
+ CArray.int32(kdims.inject(1, :*)).seq!)
572
+ [ccat, kdims, gslots, bslots, gaxes]
573
+ end
574
+
575
+ # Iterate the band positions of a band-preserving grouping. For each band
576
+ # position it yields the group block grouped by the composite categorical,
577
+ # together with two indices: val_idx (source order -- nil across the group
578
+ # axes, the band coordinate at each band axis -- selecting the group block and
579
+ # placing a source-shaped result) and out_idx (slot order -- nil across the
580
+ # group slots, the band coordinate at each band slot -- placing a slot-shaped
581
+ # result), plus the raw band coordinates. Only one group block's grouped copy
582
+ # is alive at a time (peak = O(group-block size); no whole-array grouped copy).
583
+ # Band positions run row-major over the band slots (slot order).
584
+ def each_band_block (ccat, gslots, bslots, gaxes)
585
+ shape = value.shape
586
+ ndim = shape.size
587
+ nslots = spec.slot_meta.size
588
+ band_slot_pos = []
589
+ spec.slot_meta.each_with_index { |m, s| band_slot_pos << s if m[:kind] == :band }
590
+ blens = bslots.map { |m| m[:len] }
591
+ blens.inject(1, :*).times do |t|
592
+ coords = Array.new(bslots.size)
593
+ rem = t
594
+ (bslots.size - 1).downto(0) { |j| coords[j] = rem % blens[j]; rem /= blens[j] }
595
+ val_idx = Array.new(ndim)
596
+ gaxes.each { |a| val_idx[a] = nil }
597
+ bslots.each_with_index { |m, j| val_idx[m[:axis]] = coords[j] }
598
+ out_idx = Array.new(nslots) # nil across group slots
599
+ band_slot_pos.each_with_index { |s, j| out_idx[s] = coords[j] }
600
+ yield val_idx, out_idx, coords, value[*val_idx].group_by_category(ccat)
601
+ end
602
+ end
603
+
604
+ # Dispatch a segment scan (cumsum / cumprod / cummax / cummin / cumcount). No
605
+ # :group -> the value's same-named scan; axis: :group -> per-group running
606
+ # statistic via the fused scan kernel. Folding a band into a scan (axis:
607
+ # [:group, k]) would collapse an axis, so it is rejected (a scan preserves the
608
+ # source shape).
609
+ def scan_op (op, kw)
610
+ has_group, fused = AxisGroup.parse_axis(kw[:axis])
611
+ unless has_group
612
+ unless value.respond_to?(op)
613
+ raise NotImplementedError, "axis_group scan: value has no ##{op}"
614
+ end
615
+ return value.send(op)
616
+ end
617
+ unless fused.empty?
618
+ raise ArgumentError,
619
+ "axis_group scan: folding a band into a scan (axis: [:group, k]) " \
620
+ "is not supported; a scan preserves the source shape"
621
+ end
622
+ group_axes, bundles, = spec.reduce_plan([])
623
+ value.__axis_group_scan__(group_axes, bundles, op)
624
+ end
625
+
626
+ # Dispatch an order statistic (median / percentile / quantile). No :group ->
627
+ # plain value reduction; :group -> per-group order statistics via the composite
628
+ # materialize. Folding a band into an order statistic (axis: [:group, k])
629
+ # gathers a group axis and a band axis into one statistic, a different
630
+ # operation, and remains a follow-up.
631
+ def order_stat (op, args, kw)
632
+ has_group, fused = AxisGroup.parse_axis(kw[:axis])
633
+ return value.send(op, *args) unless has_group
634
+ unless fused.empty?
635
+ raise NotImplementedError,
636
+ "axis_group: folding a band into an order statistic (axis: [:group, k]) " \
637
+ "is not supported; order statistics gather every member of a group"
638
+ end
639
+ composite_order(op, args)
640
+ end
641
+
642
+ # Per-group order statistic over the composite grouping. A flat grouping (no
643
+ # band axes) reduces the whole value through the composite categorical and
644
+ # reshapes the length-K result to the group-slot k dims (slot order). A
645
+ # band-preserving grouping runs the per-band-block materialize: output shape =
646
+ # slot order (each group slot -> its k, each band slot -> its length), each
647
+ # band position's length-K result written into the group-slot subspace.
648
+ # quantile emits five outputs. Empty / all-masked groups are UNDEF cells
649
+ # (inherited from the categorical engine).
650
+ def composite_order (op, args)
651
+ ccat, kdims, gslots, bslots, gaxes = composite_layout
652
+ nout = (op == :quantile) ? 5 : 1
653
+ if bslots.empty?
654
+ res = value.group_by_category(ccat).send(op, *args)
655
+ return nout == 1 ? res.reshape(*kdims) : res.map { |r| r.reshape(*kdims) }
656
+ end
657
+ out_shape = spec.slot_meta.map { |m| m[:kind] == :group ? m[:k] : m[:len] }
658
+ outs = Array.new(nout) { CArray.float64(*out_shape) }
659
+ each_band_block(ccat, gslots, bslots, gaxes) do |_vi, out_idx, _co, gi|
660
+ result = gi.send(op, *args)
661
+ if nout == 1
662
+ outs[0][*out_idx] = result.reshape(*kdims)
663
+ else
664
+ result.each_with_index { |r, q| outs[q][*out_idx] = r.reshape(*kdims) }
665
+ end
666
+ end
667
+ nout == 1 ? outs[0] : outs
668
+ end
669
+
670
+ # Per-band-block sorted flat source addresses. Every band position contributes
671
+ # the same nvalid = ccat.category_sizes.sum classified cells (the group
672
+ # classification does not depend on the band position), so the output is a
673
+ # regular length nband*nvalid int64: band positions row-major (slot order),
674
+ # each holding that block's addresses composite-group-major (split by
675
+ # ccat.category_sizes), each group ascending by value. A group-block-local sort
676
+ # address is lifted to the full flat source address by base + sum over the
677
+ # group axes of coord * row-major-stride, base being the band coordinates'
678
+ # contribution; the per-axis ramps are broadcast into the group-block rank.
679
+ # Masked values sort to the tail of their group (as CArray#sort). Peak stays
680
+ # O(group-block size) -- one block's grouped copy at a time.
681
+ def composite_band_sort_addr (ccat, gslots, bslots, gaxes)
682
+ shape = value.shape
683
+ strides = Array.new(shape.size)
684
+ acc = 1
685
+ (shape.size - 1).downto(0) { |a| strides[a] = acc; acc *= shape[a] }
686
+ gb_rank = gaxes.size
687
+ gb_shape = gaxes.map { |a| shape[a] }
688
+ gb_size = gb_shape.inject(1, :*)
689
+ nvalid = ccat.category_sizes.int64.sum
690
+ nband = bslots.map { |m| m[:len] }.inject(1, :*)
691
+ out = CArray.int64(nband * nvalid)
692
+ cursor = 0
693
+ each_band_block(ccat, gslots, bslots, gaxes) do |_vi, _oi, coords, gi|
694
+ base = bslots.each_with_index.inject(0) { |s, (m, j)| s + coords[j] * strides[m[:axis]] }
695
+ addr_block = CArray.int64(*gb_shape)
696
+ addr_block[] = base
697
+ gaxes.each_with_index do |a, p|
698
+ target = Array.new(gb_rank, 1)
699
+ target[p] = shape[a]
700
+ addr_block = addr_block + (CArray.int64(shape[a]).seq! * strides[a]).reshape(*target)
701
+ end
702
+ local = gi.sort_addr # into the group block
703
+ out[cursor...cursor + nvalid] = addr_block.reshape(gb_size)[local]
704
+ cursor += nvalid
705
+ end
706
+ out
707
+ end
708
+ end
709
+
710
+ require "carray/categorical"
711
+ CArray.__register_axis_group_classes__(CACategorical, AxisGroup)