carray 2.0.1 → 3.0.1

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 (386) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -25
  3. data/CHANGELOG.md +338 -0
  4. data/{NEWS.md → CHANGELOG.v1.md} +3 -0
  5. data/LICENSE +1 -1
  6. data/README.md +120 -36
  7. data/carray.gemspec +32 -30
  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 +1244 -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 +124 -0
  15. data/ext/ca_binop_dispatch.h +152 -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 +239 -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 +4367 -0
  26. data/ext/ca_kernel_iterator.h +2596 -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 +839 -0
  33. data/ext/ca_obj_binop.c +948 -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 +967 -0
  39. data/ext/ca_obj_face.c +750 -0
  40. data/ext/ca_obj_face.h +279 -0
  41. data/ext/ca_obj_fake.c +239 -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 +308 -0
  45. data/ext/ca_obj_grid.c +866 -440
  46. data/ext/ca_obj_meld.c +1039 -0
  47. data/ext/ca_obj_moncmp.c +588 -0
  48. data/ext/ca_obj_monop.c +1123 -0
  49. data/ext/ca_obj_object.c +866 -296
  50. data/ext/ca_obj_record.c +470 -0
  51. data/ext/ca_obj_reduce.c +97 -82
  52. data/ext/ca_obj_refer.c +593 -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 +624 -0
  56. data/ext/ca_obj_select.c +344 -296
  57. data/ext/ca_obj_select_axis.c +1306 -0
  58. data/ext/ca_obj_shift.c +231 -793
  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 +2584 -0
  62. data/ext/ca_obj_string.c +270 -0
  63. data/ext/ca_obj_tile.c +622 -0
  64. data/ext/ca_obj_time.c +548 -0
  65. data/ext/ca_obj_timedelta.c +437 -0
  66. data/ext/ca_obj_transpose.c +62 -516
  67. data/ext/ca_obj_triop.c +785 -0
  68. data/ext/ca_obj_window.c +1202 -565
  69. data/ext/ca_op_byte_swap.c +175 -0
  70. data/ext/ca_op_cmplx64.h +123 -0
  71. data/ext/ca_op_ipower.c +316 -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 +473 -0
  75. data/ext/ca_sweep_engine.h +166 -0
  76. data/ext/ca_transform_common.c +235 -0
  77. data/ext/ca_triop_dispatch.c +55 -0
  78. data/ext/ca_triop_dispatch.h +62 -0
  79. data/ext/carray.h +810 -420
  80. data/ext/carray_access.c +873 -731
  81. data/ext/carray_attribute.c +98 -329
  82. data/ext/carray_bincount.c +255 -0
  83. data/ext/carray_broadcast.c +376 -0
  84. data/ext/carray_build_flags.h +3 -0
  85. data/ext/carray_call_cfunc.c +2897 -874
  86. data/ext/carray_call_cfunc.h +313 -0
  87. data/ext/carray_cast.c +1264 -315
  88. data/ext/carray_cast_func.rb +81 -40
  89. data/ext/carray_class.c +53 -63
  90. data/ext/carray_config.h +28 -0
  91. data/ext/carray_conversion.c +350 -346
  92. data/ext/carray_copy.c +168 -270
  93. data/ext/carray_core.c +1396 -206
  94. data/ext/carray_count.c +312 -0
  95. data/ext/carray_data_type.c +43 -19
  96. data/ext/carray_element.c +585 -213
  97. data/ext/carray_factorize.c +2542 -0
  98. data/ext/carray_generate.c +230 -559
  99. data/ext/carray_histogram.c +490 -0
  100. data/ext/carray_hold.c +228 -0
  101. data/ext/carray_index_classifier.c +1021 -0
  102. data/ext/carray_index_classifier.h +27 -0
  103. data/ext/carray_internal.h +136 -0
  104. data/ext/carray_kernels_bincmp.c +4446 -0
  105. data/ext/carray_kernels_binop.c +11001 -0
  106. data/ext/carray_kernels_init.c +1131 -0
  107. data/ext/carray_kernels_map.c +3467 -0
  108. data/ext/carray_kernels_moncmp.c +2097 -0
  109. data/ext/carray_kernels_monop.c +18313 -0
  110. data/ext/carray_kernels_reduce_aggregate.c +25837 -0
  111. data/ext/carray_kernels_reduce_boolean.c +330 -0
  112. data/ext/carray_kernels_reduce_cumulative.c +14593 -0
  113. data/ext/carray_kernels_reduce_extreme.c +16948 -0
  114. data/ext/carray_kernels_reduce_variance.c +3910 -0
  115. data/ext/carray_kernels_scan.c +3693 -0
  116. data/ext/carray_kernels_search.c +32138 -0
  117. data/ext/carray_kernels_sort.c +10626 -0
  118. data/ext/carray_kernels_triop.c +1392 -0
  119. data/ext/carray_lazy.c +737 -0
  120. data/ext/carray_loop.c +88 -200
  121. data/ext/carray_mask.c +853 -158
  122. data/ext/carray_math_kernel.h +120 -0
  123. data/ext/carray_mathfunc.c +10 -241
  124. data/ext/carray_median_percentile.c +1257 -0
  125. data/ext/carray_memory_view.c +1650 -0
  126. data/ext/carray_operator.c +1525 -320
  127. data/ext/carray_order.c +664 -1394
  128. data/ext/carray_partition.c +416 -0
  129. data/ext/carray_random.c +518 -0
  130. data/ext/carray_scatter.c +357 -0
  131. data/ext/carray_slab.c +1219 -0
  132. data/ext/carray_slab.h +84 -0
  133. data/ext/carray_sort.c +829 -0
  134. data/ext/carray_sort_kernel.c +620 -0
  135. data/ext/carray_struct.c +695 -0
  136. data/ext/carray_test.c +343 -229
  137. data/ext/carray_undef.c +34 -17
  138. data/ext/carray_utils.c +175 -74
  139. data/ext/extconf.rb +234 -55
  140. data/ext/mk_call_cfunc.rb +671 -0
  141. data/ext/mkkernel.rb +9096 -0
  142. data/ext/ruby_carray.c +211 -108
  143. data/ext/version.h +4 -14
  144. data/ext/version.rb +5 -13
  145. data/lib/carray/arrow_tensor.rb +401 -0
  146. data/lib/carray/attribute.rb +166 -0
  147. data/lib/carray/autoload_carray.rb +239 -0
  148. data/lib/carray/autoload_method_extension.rb +45 -0
  149. data/lib/carray/axis_group.rb +711 -0
  150. data/lib/carray/basics.rb +481 -0
  151. data/lib/carray/bincount_nd.rb +358 -0
  152. data/lib/carray/block_iterator.rb +614 -0
  153. data/lib/carray/boolean_reduce.rb +109 -0
  154. data/lib/carray/categorical.rb +561 -0
  155. data/lib/carray/categorical_iterator.rb +1084 -0
  156. data/lib/carray/complex.rb +150 -0
  157. data/lib/carray/conditional.rb +216 -0
  158. data/lib/carray/const_string.rb +228 -0
  159. data/lib/carray/construct.rb +160 -328
  160. data/lib/carray/core_extensions.rb +297 -0
  161. data/lib/carray/data_type_extension.rb +250 -0
  162. data/lib/carray/fixlen_string.rb +95 -0
  163. data/lib/carray/frame/concat.rb +132 -0
  164. data/lib/carray/frame/convert.rb +95 -0
  165. data/lib/carray/frame/csv_parser.rb +211 -0
  166. data/lib/carray/frame/frame.rb +642 -0
  167. data/lib/carray/frame/group.rb +186 -0
  168. data/lib/carray/frame/io.rb +321 -0
  169. data/lib/carray/frame/join.rb +248 -0
  170. data/lib/carray/frame/records.rb +99 -0
  171. data/lib/carray/frame/sort.rb +113 -0
  172. data/lib/carray/frame/verbs.rb +316 -0
  173. data/lib/carray/frame.rb +16 -0
  174. data/lib/carray/fuse_source.rb +123 -0
  175. data/lib/carray/fusion.rb +218 -0
  176. data/lib/carray/histogram.rb +512 -0
  177. data/lib/carray/inspect.rb +37 -26
  178. data/lib/carray/iterator.rb +58 -349
  179. data/lib/carray/lazy.rb +941 -0
  180. data/lib/carray/mask_gap_fill.rb +200 -0
  181. data/lib/carray/math.rb +78 -342
  182. data/lib/carray/meld_reduce.rb +289 -0
  183. data/lib/carray/methods/align_addr.rb +116 -0
  184. data/lib/carray/methods/bin.rb +128 -0
  185. data/lib/carray/methods/bincount.rb +87 -0
  186. data/lib/carray/methods/bit_string.rb +92 -0
  187. data/lib/carray/methods/broadcast.rb +63 -0
  188. data/lib/carray/methods/choose.rb +39 -0
  189. data/lib/carray/methods/composition.rb +280 -0
  190. data/lib/carray/methods/gather_nd.rb +206 -0
  191. data/lib/carray/methods/index.rb +39 -0
  192. data/lib/carray/methods/insert_block.rb +99 -0
  193. data/lib/carray/methods/is_in.rb +141 -0
  194. data/lib/carray/methods/join.rb +90 -0
  195. data/lib/carray/methods/locate_addr.rb +52 -0
  196. data/lib/carray/methods/mask_duplicates.rb +41 -0
  197. data/lib/carray/methods/meshgrid.rb +90 -0
  198. data/lib/carray/methods/mode.rb +126 -0
  199. data/lib/carray/methods/nunique.rb +46 -0
  200. data/lib/carray/methods/resize.rb +56 -0
  201. data/lib/carray/methods/snap.rb +161 -0
  202. data/lib/carray/methods/string_format.rb +57 -0
  203. data/lib/carray/methods/unique.rb +47 -0
  204. data/lib/carray/methods/value_counts.rb +71 -0
  205. data/lib/carray/mkmf.rb +124 -101
  206. data/lib/carray/runtime.rb +89 -0
  207. data/lib/carray/serialize.rb +478 -167
  208. data/lib/carray/slab_iterator.rb +305 -0
  209. data/lib/carray/stack.rb +291 -0
  210. data/lib/carray/string.rb +56 -180
  211. data/lib/carray/string_operation_extension.rb +289 -0
  212. data/lib/carray/struct.rb +335 -323
  213. data/lib/carray/struct_builder.rb +697 -0
  214. data/lib/carray/table.rb +41 -2
  215. data/lib/carray/time.rb +2654 -38
  216. data/lib/carray/window_iterator.rb +927 -0
  217. data/lib/carray.rb +55 -57
  218. data/yard-stubs/ca_obj_array.rb +385 -0
  219. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  220. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  221. data/yard-stubs/ca_obj_block.rb +73 -0
  222. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  223. data/yard-stubs/ca_obj_fake.rb +31 -0
  224. data/yard-stubs/ca_obj_farray.rb +32 -0
  225. data/yard-stubs/ca_obj_field.rb +45 -0
  226. data/yard-stubs/ca_obj_grid.rb +35 -0
  227. data/yard-stubs/ca_obj_refer.rb +72 -0
  228. data/yard-stubs/ca_obj_roll.rb +45 -0
  229. data/yard-stubs/ca_obj_shift.rb +43 -0
  230. data/yard-stubs/ca_obj_stride.rb +181 -0
  231. data/yard-stubs/ca_obj_tile.rb +29 -0
  232. data/yard-stubs/ca_obj_transpose.rb +40 -0
  233. data/yard-stubs/ca_obj_window.rb +49 -0
  234. data/yard-stubs/carray_access.rb +131 -0
  235. data/yard-stubs/carray_attribute.rb +246 -0
  236. data/yard-stubs/carray_broadcast.rb +37 -0
  237. data/yard-stubs/carray_cast.rb +489 -0
  238. data/yard-stubs/carray_class.rb +65 -0
  239. data/yard-stubs/carray_conversion.rb +76 -0
  240. data/yard-stubs/carray_copy.rb +79 -0
  241. data/yard-stubs/carray_core.rb +114 -0
  242. data/yard-stubs/carray_count.rb +79 -0
  243. data/yard-stubs/carray_element.rb +108 -0
  244. data/yard-stubs/carray_generate.rb +66 -0
  245. data/yard-stubs/carray_lazy.rb +23 -0
  246. data/yard-stubs/carray_loop.rb +140 -0
  247. data/yard-stubs/carray_mask.rb +259 -0
  248. data/yard-stubs/carray_math.rb +132 -0
  249. data/yard-stubs/carray_mathfunc.rb +45 -0
  250. data/yard-stubs/carray_median_percentile.rb +89 -0
  251. data/yard-stubs/carray_memory_view.rb +163 -0
  252. data/yard-stubs/carray_order.rb +312 -0
  253. data/yard-stubs/carray_random.rb +89 -0
  254. data/yard-stubs/carray_scatter.rb +106 -0
  255. data/yard-stubs/carray_slab.rb +57 -0
  256. data/yard-stubs/carray_sort.rb +163 -0
  257. data/yard-stubs/carray_test.rb +85 -0
  258. data/yard-stubs/carray_undef.rb +64 -0
  259. data/yard-stubs/carray_utils.rb +97 -0
  260. data/yard-stubs/ruby_carray.rb +193 -0
  261. metadata +220 -138
  262. data/Rakefile +0 -51
  263. data/TODO.md +0 -18
  264. data/ext/ca_iter_block.c +0 -257
  265. data/ext/ca_iter_dimension.c +0 -299
  266. data/ext/ca_iter_window.c +0 -214
  267. data/ext/ca_obj_mapping.c +0 -644
  268. data/ext/ca_obj_unbound_repeat.c +0 -529
  269. data/ext/carray_iterator.c +0 -641
  270. data/ext/carray_math.rb +0 -850
  271. data/ext/carray_numeric.c +0 -259
  272. data/ext/carray_sort_addr.c +0 -254
  273. data/ext/carray_stat.c +0 -2100
  274. data/ext/carray_stat_proc.rb +0 -1999
  275. data/ext/mkmath.rb +0 -741
  276. data/ext/ruby_ccomplex.c +0 -509
  277. data/ext/ruby_float_func.c +0 -86
  278. data/lib/carray/array.rb +0 -8
  279. data/lib/carray/autoload/autoload_base.rb +0 -19
  280. data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
  281. data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
  282. data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
  283. data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
  284. data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
  285. data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
  286. data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
  287. data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
  288. data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
  289. data/lib/carray/autoload/autoload_gem_random.rb +0 -8
  290. data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
  291. data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
  292. data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
  293. data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
  294. data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
  295. data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
  296. data/lib/carray/autoload/autoload_object_link.rb +0 -1
  297. data/lib/carray/autoload/autoload_object_pack.rb +0 -2
  298. data/lib/carray/autoload.rb +0 -141
  299. data/lib/carray/basic.rb +0 -191
  300. data/lib/carray/broadcast.rb +0 -101
  301. data/lib/carray/compose.rb +0 -315
  302. data/lib/carray/convert.rb +0 -115
  303. data/lib/carray/info.rb +0 -110
  304. data/lib/carray/io/imagemagick.rb +0 -235
  305. data/lib/carray/mask.rb +0 -102
  306. data/lib/carray/math/histogram.rb +0 -177
  307. data/lib/carray/math/recurrence.rb +0 -93
  308. data/lib/carray/object/ca_obj_iterator.rb +0 -50
  309. data/lib/carray/object/ca_obj_link.rb +0 -50
  310. data/lib/carray/object/ca_obj_pack.rb +0 -99
  311. data/lib/carray/obsolete.rb +0 -256
  312. data/lib/carray/ordering.rb +0 -181
  313. data/lib/carray/testing.rb +0 -51
  314. data/lib/carray/transform.rb +0 -109
  315. data/mailmap +0 -1
  316. data/misc/Methods.ja.md +0 -182
  317. data/misc/NOTE +0 -51
  318. data/spec/Classes/CABitfield_spec.rb +0 -58
  319. data/spec/Classes/CABlockIterator_spec.rb +0 -114
  320. data/spec/Classes/CABlock_spec.rb +0 -205
  321. data/spec/Classes/CAField_spec.rb +0 -39
  322. data/spec/Classes/CAGrid_spec.rb +0 -75
  323. data/spec/Classes/CAMap_spec.rb +0 -0
  324. data/spec/Classes/CAMapping_spec.rb +0 -105
  325. data/spec/Classes/CAObject_attribute_spec.rb +0 -33
  326. data/spec/Classes/CAObject_spec.rb +0 -33
  327. data/spec/Classes/CARefer_spec.rb +0 -93
  328. data/spec/Classes/CARepeat_spec.rb +0 -65
  329. data/spec/Classes/CASelect_spec.rb +0 -22
  330. data/spec/Classes/CAShift_spec.rb +0 -16
  331. data/spec/Classes/CAStruct_spec.rb +0 -71
  332. data/spec/Classes/CATranspose_spec.rb +0 -60
  333. data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
  334. data/spec/Classes/CAWindow_spec.rb +0 -54
  335. data/spec/Classes/CAWrap_spec.rb +0 -8
  336. data/spec/Classes/CArray_spec.rb +0 -184
  337. data/spec/Classes/CScalar_spec.rb +0 -55
  338. data/spec/Features/feature_130_spec.rb +0 -19
  339. data/spec/Features/feature_attributes_spec.rb +0 -280
  340. data/spec/Features/feature_boolean_spec.rb +0 -98
  341. data/spec/Features/feature_broadcast.rb +0 -116
  342. data/spec/Features/feature_cast_function.rb +0 -19
  343. data/spec/Features/feature_cast_spec.rb +0 -33
  344. data/spec/Features/feature_class_spec.rb +0 -84
  345. data/spec/Features/feature_complex_spec.rb +0 -42
  346. data/spec/Features/feature_composite_spec.rb +0 -124
  347. data/spec/Features/feature_convert_spec.rb +0 -46
  348. data/spec/Features/feature_copy_spec.rb +0 -123
  349. data/spec/Features/feature_creation_spec.rb +0 -84
  350. data/spec/Features/feature_element_spec.rb +0 -144
  351. data/spec/Features/feature_extream_spec.rb +0 -54
  352. data/spec/Features/feature_generate_spec.rb +0 -74
  353. data/spec/Features/feature_index_spec.rb +0 -69
  354. data/spec/Features/feature_mask_spec.rb +0 -580
  355. data/spec/Features/feature_math_spec.rb +0 -97
  356. data/spec/Features/feature_order_spec.rb +0 -146
  357. data/spec/Features/feature_ref_store_spec.rb +0 -209
  358. data/spec/Features/feature_serialization_spec.rb +0 -125
  359. data/spec/Features/feature_stat_spec.rb +0 -397
  360. data/spec/Features/feature_virtual_spec.rb +0 -48
  361. data/spec/Features/method_eq_spec.rb +0 -81
  362. data/spec/Features/method_is_nan_spec.rb +0 -12
  363. data/spec/Features/method_map_spec.rb +0 -54
  364. data/spec/Features/method_max_with.rb +0 -20
  365. data/spec/Features/method_min_with.rb +0 -19
  366. data/spec/Features/method_ne_spec.rb +0 -18
  367. data/spec/Features/method_project_spec.rb +0 -188
  368. data/spec/Features/method_ref_spec.rb +0 -27
  369. data/spec/Features/method_round_spec.rb +0 -11
  370. data/spec/Features/method_s_linspace_spec.rb +0 -48
  371. data/spec/Features/method_s_span_spec.rb +0 -14
  372. data/spec/Features/method_seq_spec.rb +0 -47
  373. data/spec/Features/method_sort_with.rb +0 -43
  374. data/spec/Features/method_sorted_with.rb +0 -29
  375. data/spec/Features/method_span_spec.rb +0 -42
  376. data/spec/Features/method_wrap_readonly_spec.rb +0 -43
  377. data/spec/UnitTest/test_CAVirtual.rb +0 -214
  378. data/spec/spec_all.rb +0 -10
  379. data/utils/ca_ase.rb +0 -21
  380. data/utils/ca_methods.rb +0 -15
  381. data/utils/cast_checker.rb +0 -30
  382. data/utils/convert_test.rb +0 -73
  383. data/utils/extract_yard.rb +0 -22
  384. data/utils/guess_shape.rb +0 -76
  385. data/utils/monkey_patch_methods.rb +0 -62
  386. data/utils/remove_resource_fork.sh +0 -5
@@ -0,0 +1,1244 @@
1
+ /* ---------------------------------------------------------------------------
2
+
3
+ Axis-group compute kernels: the grouped reduction and the grouped segment
4
+ scan.
5
+
6
+ INTERNAL kernels behind `__...__`-named methods. The user-facing surface
7
+ (CACategorical / AxisGroup type gate in `[]`, CAGroupIterator, the :group
8
+ reduce dispatch, GroupLabels) is wired in ca_group_iter.c (the `[]` gate and
9
+ the iterator) + `axis_group` (lib/carray/axis_group.rb). Both kernels are
10
+ written in the general form: several group axes + rank-N categorical (an N-D
11
+ codes map) via a per-slab-element composite code, native data type dispatch,
12
+ mask support. They take pre-built code bundles, so they are independent of
13
+ how the classifier is constructed.
14
+
15
+ Mechanism, shared by both kernels:
16
+ - CA_FOR_EACH_SLAB pins the union of grouped source axes as the slab and
17
+ walks the band (= non-grouped) axes in the outer iter.
18
+ - For each slab element a composite group code is computed on the fly from
19
+ the per-bundle code tables (per-axis code, flat-collapsed; a rank-N
20
+ categorical = one bundle consuming more than one source axis). The
21
+ composite code is never materialised — it is a per-element local.
22
+ - reduce: scatter out[composite * band + b] op= value, sort-free. Peak
23
+ memory = output + per-group accumulators (O(K*band)), never O(N).
24
+ - scan: emit the running value per element into a source-shaped output
25
+ (see the section comment above `rb_ca_axis_group_scan`).
26
+
27
+ Group codes outside [0, k) at any bundle mark the slab element unassigned
28
+ (skipped, contributing to no group) — mirrors digitize OOB.
29
+
30
+ Float reductions carry the CArray ε-close contract: relative error bounded,
31
+ not bit-exact.
32
+
33
+ --------------------------------------------------------------------------- */
34
+
35
+ #include "carray.h"
36
+ #include "ca_kernel_iterator.h"
37
+ #include <math.h>
38
+
39
+ /* op codes */
40
+ enum {
41
+ GR_SUM = 0, GR_PROD, GR_MEAN, GR_MIN, GR_MAX,
42
+ GR_VARIANCE, GR_STDDEV, GR_COUNT, GR_ALL, GR_ANY,
43
+ GR_VARIANCEP, GR_STDDEVP, GR_MINADDR, GR_MAXADDR,
44
+ GR_ACCUM
45
+ };
46
+
47
+ static int
48
+ group_op_code (VALUE vop)
49
+ {
50
+ ID id = SYM2ID(vop);
51
+ if ( id == rb_intern("sum") ) return GR_SUM;
52
+ else if ( id == rb_intern("accumulate") ) return GR_ACCUM;
53
+ else if ( id == rb_intern("prod") ) return GR_PROD;
54
+ else if ( id == rb_intern("mean") ) return GR_MEAN;
55
+ else if ( id == rb_intern("min") ) return GR_MIN;
56
+ else if ( id == rb_intern("max") ) return GR_MAX;
57
+ else if ( id == rb_intern("variance") ) return GR_VARIANCE;
58
+ else if ( id == rb_intern("stddev") ) return GR_STDDEV;
59
+ else if ( id == rb_intern("variancep") ) return GR_VARIANCEP;
60
+ else if ( id == rb_intern("stddevp") ) return GR_STDDEVP;
61
+ else if ( id == rb_intern("count") ) return GR_COUNT;
62
+ else if ( id == rb_intern("count_not_masked") ) return GR_COUNT;
63
+ else if ( id == rb_intern("min_addr") ) return GR_MINADDR;
64
+ else if ( id == rb_intern("max_addr") ) return GR_MAXADDR;
65
+ else if ( id == rb_intern("all") ) return GR_ALL;
66
+ else if ( id == rb_intern("any") ) return GR_ANY;
67
+ rb_raise(rb_eArgError, "axis_group_reduce: unsupported op :%s",
68
+ rb_id2name(id));
69
+ }
70
+
71
+ /* ---- per-slab-element composite-code walk, templated on the load type ----
72
+
73
+ Reads `*(T *)(p + data_off)` for each slab element, computes the composite
74
+ group code, applies the mask, and folds the value into whichever
75
+ accumulator buffers are non-NULL. Accumulation is always in `double` (or
76
+ counts in ca_size_t), so the op-finalisation below is type-agnostic — only
77
+ the LOAD is monomorphised per data type, keeping the inner loop autovectorisable
78
+ while avoiding a forced float64 materialise of the (large) source. */
79
+
80
+ /* GROUP_WALK(T, ACCUM): one CA_FOR_EACH_SLAB pass. For each slab element it
81
+ computes the composite group code, applies the mask, then runs ACCUM with
82
+ `v` (the element, widened to double) and `o` (the output flat index =
83
+ code * band + band_flat) in scope. ACCUM is the only per-op-varying part,
84
+ so the data type is monomorphised once per type while sum / mean / variance /
85
+ ... reuse the same walk. */
86
+ /* Sentinel o-code for a slab element whose composite code is out of range
87
+ (excluded categorical), stored in the precomputed plan below. */
88
+ #define GW_SKIP (~(ca_size_t) 0)
89
+
90
+ /* CAREFUL: the slab-emission counter `b` is used directly as the band flat
91
+ index of the output — here and in the scan walks below. That identity holds
92
+ only because the outer iter advances row-major over the complement axes in
93
+ ascending source order (next_slab_axes in ca_kernel_iterator.c). If that
94
+ iteration order changes, every band lands in the wrong output row and
95
+ nothing raises. */
96
+
97
+ /* The composite code, data/mask byte offsets and (for min_addr / max_addr) the
98
+ group-relative source address of a slab element depend only on the slab
99
+ odometer position, not on the band. So they are identical for every band.
100
+ Precompute them once (on the first band, O(slab_elements) = O(group_prod)
101
+ metadata) and let each band pass just gather the plan + scatter. This keeps
102
+ the composite-code bundle walk and the odometer out of the per-(band x
103
+ element) hot path. Peak memory stays O(output + group_prod), not O(input). */
104
+ #define GROUP_WALK(T, ACCUM) \
105
+ do { \
106
+ ca_iter_state st; \
107
+ char *p; \
108
+ boolean8_t *m; \
109
+ ca_size_t b = 0; \
110
+ ca_size_t *gw_ocode = NULL, *gw_doff = NULL, *gw_moff = NULL, \
111
+ *gw_gaddr = NULL; \
112
+ int gw_ready = 0; \
113
+ int gw_need_addr = ( op == GR_MINADDR || op == GR_MAXADDR ); \
114
+ CA_FOR_EACH_SLAB(st, ca, axes, (int8_t) ngroup, CA_KERNEL_READ, p, m) { \
115
+ int8_t sndim = st.slab_ndim; \
116
+ ca_size_t SE = st.slab_elements; \
117
+ if ( SE > 0 ) { \
118
+ if ( ! gw_ready ) { \
119
+ gw_ocode = ALLOC_N(ca_size_t, SE); \
120
+ gw_doff = ALLOC_N(ca_size_t, SE); \
121
+ gw_moff = ALLOC_N(ca_size_t, SE); \
122
+ gw_gaddr = ALLOC_N(ca_size_t, SE); \
123
+ ca_size_t sidx[CA_RANK_MAX]; \
124
+ for ( int8_t k = 0; k < sndim; k++ ) sidx[k] = 0; \
125
+ for ( ca_size_t e = 0; e < SE; e++ ) { \
126
+ /* composite group code from the per-bundle code tables */ \
127
+ int skip = 0; \
128
+ ca_size_t code = 0; \
129
+ for ( int bi = 0; bi < n_bundles; bi++ ) { \
130
+ ca_size_t sub = 0; \
131
+ for ( int j = 0; j < bundle_nconsumed[bi]; j++ ) \
132
+ sub += sidx[ bundle_slot[bi][j] ] * bundle_cstride[bi][j]; \
133
+ int32_t c = bundle_codes[bi][sub]; \
134
+ if ( c < 0 || (ca_size_t) c >= bundle_k[bi] ) { skip = 1; break; } \
135
+ code += (ca_size_t) c * bundle_placeval[bi]; \
136
+ } \
137
+ ca_size_t doff = 0, moff = 0, gaddr = 0; \
138
+ for ( int8_t k = 0; k < sndim; k++ ) { \
139
+ doff += sidx[k] * st.slab_strides[k]; \
140
+ if ( m ) moff += sidx[k] * st.slab_mask_strides[k]; \
141
+ } \
142
+ if ( gw_need_addr ) \
143
+ for ( long s = 0; s < ngroup; s++ ) \
144
+ gaddr += (ca_size_t) sidx[s] * grstride[s]; \
145
+ gw_ocode[e] = skip ? GW_SKIP : code * band; \
146
+ gw_doff[e] = doff; \
147
+ gw_moff[e] = moff; \
148
+ gw_gaddr[e] = gaddr; \
149
+ /* odometer advance (last slab axis ticks fastest) */ \
150
+ for ( int8_t k = sndim - 1; k >= 0; k-- ) { \
151
+ if ( ++sidx[k] < st.slab_dims[k] ) break; \
152
+ sidx[k] = 0; \
153
+ } \
154
+ } \
155
+ gw_ready = 1; \
156
+ } \
157
+ for ( ca_size_t e = 0; e < SE; e++ ) { \
158
+ ca_size_t oc = gw_ocode[e]; \
159
+ if ( oc == GW_SKIP ) continue; \
160
+ if ( m && m[ gw_moff[e] ] ) continue; \
161
+ double v = (double) ( *(T *)(p + gw_doff[e]) ); \
162
+ ca_size_t o = oc + b; \
163
+ ca_size_t gaddr = gw_gaddr[e]; \
164
+ ACCUM; \
165
+ (void) v; (void) o; (void) gaddr; \
166
+ } \
167
+ } \
168
+ b++; \
169
+ } \
170
+ if ( gw_ocode ) xfree(gw_ocode); \
171
+ if ( gw_doff ) xfree(gw_doff); \
172
+ if ( gw_moff ) xfree(gw_moff); \
173
+ if ( gw_gaddr ) xfree(gw_gaddr); \
174
+ } while (0)
175
+
176
+ /* Run one walk over every supported native data type. Dispatched on the source
177
+ data_type so the inner loop stays monomorphic (no forced float64 cast). */
178
+ #define GROUP_DISPATCH(ACCUM) \
179
+ switch ( ca->data_type ) { \
180
+ case CA_BOOLEAN: GROUP_WALK(boolean8_t, ACCUM); break; \
181
+ case CA_INT8: GROUP_WALK(int8_t, ACCUM); break; \
182
+ case CA_UINT8: GROUP_WALK(uint8_t, ACCUM); break; \
183
+ case CA_INT16: GROUP_WALK(int16_t, ACCUM); break; \
184
+ case CA_UINT16: GROUP_WALK(uint16_t, ACCUM); break; \
185
+ case CA_INT32: GROUP_WALK(int32_t, ACCUM); break; \
186
+ case CA_UINT32: GROUP_WALK(uint32_t, ACCUM); break; \
187
+ case CA_INT64: GROUP_WALK(int64_t, ACCUM); break; \
188
+ case CA_UINT64: GROUP_WALK(uint64_t, ACCUM); break; \
189
+ case CA_FLOAT32: GROUP_WALK(float, ACCUM); break; \
190
+ case CA_FLOAT64: GROUP_WALK(double, ACCUM); break; \
191
+ default: break; \
192
+ }
193
+
194
+ /* `accumulate` is the one op that folds in the SOURCE's own type instead of in
195
+ double, so it wraps at that width exactly as CArray#accumulate does. It
196
+ folds straight into the (zeroed) output — whose data type is the source's —
197
+ so an empty group already holds the additive identity 0 and needs no mask.
198
+ The type cannot travel inside a GROUP_WALK ACCUM argument (an argument's own
199
+ tokens are not substituted for the macro's parameters), so the walk below
200
+ names its type twice. */
201
+ #define GACC_ADD(T) ( ((T *) co->ptr)[o] += *(T *)(p + gw_doff[e]) )
202
+ /* A boolean accumulate is XOR parity, matching the core: the result stays
203
+ boolean, so a second `true` has nowhere to carry into. */
204
+ #define GACC_XOR \
205
+ ( ((boolean8_t *) co->ptr)[o] ^= (*(boolean8_t *)(p + gw_doff[e]) ? 1 : 0) )
206
+
207
+ /* __axis_group_reduce__(group_axes, bundles, op) — group-reduces self along
208
+ * the union of `group_axes` (ascending source-axis indices = the slab) into
209
+ * composite groups described by `bundles`, preserving the band (= non-grouped)
210
+ * axes. Internal: the Ruby surface is CAGroupIterator.
211
+
212
+ bundles: Array of [codes, k, bundle_axes]
213
+ - codes : integer CArray, row-major over `bundle_axes` dims, giving
214
+ the group code in [0, k) for each cell of the consumed
215
+ axes (rank-1 = per-axis classifier; rank-N = one bundle
216
+ consuming >1 source axes = non-rectangular categorical).
217
+ - k : Integer, group count for this bundle.
218
+ - bundle_axes : Array of source-axis indices this bundle consumes
219
+ (in the row-major order matching `codes`).
220
+
221
+ The bundles' axes together equal `group_axes` (as a set). Output shape is
222
+ [K_total, *band_dims] with K_total = Π k over bundles (bundle 0 most
223
+ significant, row-major) and band_dims = the non-grouped source dims in
224
+ ascending order. The Ruby surface reshapes the leading K_total axis into
225
+ the individual group axes.
226
+
227
+ op: :sum :prod :mean :min :max :variance :stddev :variancep :stddevp
228
+ :count (:count_not_masked is a synonym) :min_addr :max_addr :all :any.
229
+ :min_addr / :max_addr return the flat raveled source address of the group's
230
+ extremum, not a group-local index.
231
+
232
+ Empty / all-masked groups follow the CArray zero-contribution contract: an
233
+ identity-bearing op returns its identity (sum 0, prod 1, count 0, all true,
234
+ any false), a ratio / extremum returns UNDEF. Sample variance / stddev:
235
+ n == 0 UNDEF, n == 1 -> 0.0, n >= 2 the formula.
236
+ */
237
+ static VALUE
238
+ rb_ca_axis_group_reduce (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
239
+ {
240
+ CArray *src, *ca, *co;
241
+ int op = group_op_code(vop);
242
+
243
+ GetCArray(self, src);
244
+
245
+ if ( src->ndim <= 0 ) {
246
+ rb_raise(rb_eRuntimeError, "axis_group_reduce: scalar source");
247
+ }
248
+
249
+ /* --- group (slab) axes --- */
250
+ Check_Type(vgaxes, T_ARRAY);
251
+ long ngroup = RARRAY_LEN(vgaxes);
252
+ if ( ngroup <= 0 || ngroup > src->ndim ) {
253
+ rb_raise(rb_eArgError, "axis_group_reduce: bad group axis count %ld", ngroup);
254
+ }
255
+ int8_t axes[CA_RANK_MAX];
256
+ char is_group[CA_RANK_MAX];
257
+ for ( int8_t i = 0; i < src->ndim; i++ ) is_group[i] = 0;
258
+ ca_size_t group_prod = 1;
259
+ for ( long i = 0; i < ngroup; i++ ) {
260
+ int a = NUM2INT(RARRAY_AREF(vgaxes, i));
261
+ if ( a < 0 || a >= src->ndim ) {
262
+ rb_raise(rb_eArgError, "axis_group_reduce: group axis %d out of range", a);
263
+ }
264
+ if ( is_group[a] ) {
265
+ rb_raise(rb_eArgError, "axis_group_reduce: duplicate group axis %d", a);
266
+ }
267
+ if ( i > 0 && a <= NUM2INT(RARRAY_AREF(vgaxes, i - 1)) ) {
268
+ rb_raise(rb_eArgError, "axis_group_reduce: group axes must be ascending");
269
+ }
270
+ is_group[a] = 1;
271
+ axes[i] = (int8_t) a;
272
+ group_prod *= src->dim[a];
273
+ }
274
+
275
+ /* --- bundles: small per-group code tables (metadata, kept alive) --- */
276
+ Check_Type(vbundles, T_ARRAY);
277
+ int n_bundles = (int) RARRAY_LEN(vbundles);
278
+ if ( n_bundles <= 0 || n_bundles > CA_RANK_MAX ) {
279
+ rb_raise(rb_eArgError, "axis_group_reduce: bad bundle count %d", n_bundles);
280
+ }
281
+ int32_t *bundle_codes[CA_RANK_MAX];
282
+ ca_size_t bundle_k[CA_RANK_MAX];
283
+ ca_size_t bundle_placeval[CA_RANK_MAX];
284
+ int bundle_nconsumed[CA_RANK_MAX];
285
+ int bundle_slot[CA_RANK_MAX][CA_RANK_MAX];
286
+ ca_size_t bundle_cstride[CA_RANK_MAX][CA_RANK_MAX];
287
+ CArray *bundle_ca[CA_RANK_MAX];
288
+ volatile VALUE keep = rb_ary_new(); /* GC-pin int32 code views */
289
+
290
+ ca_size_t K_total = 1;
291
+ long consumed_total = 0;
292
+ for ( int bi = 0; bi < n_bundles; bi++ ) {
293
+ VALUE bundle = RARRAY_AREF(vbundles, bi);
294
+ Check_Type(bundle, T_ARRAY);
295
+ if ( RARRAY_LEN(bundle) != 3 ) {
296
+ rb_raise(rb_eArgError, "axis_group_reduce: bundle must be [codes, k, axes]");
297
+ }
298
+ VALUE vcodes = RARRAY_AREF(bundle, 0);
299
+ ca_size_t k = (ca_size_t) NUM2LONG(RARRAY_AREF(bundle, 1));
300
+ VALUE vbaxes = RARRAY_AREF(bundle, 2);
301
+ Check_Type(vbaxes, T_ARRAY);
302
+ if ( k <= 0 ) {
303
+ rb_raise(rb_eArgError, "axis_group_reduce: bundle k must be positive");
304
+ }
305
+
306
+ /* int32 view of the codes (metadata-sized, O(consumed-axis dims)) */
307
+ VALUE v32 = rb_ca_wrap_readonly(vcodes, INT2NUM(CA_INT32));
308
+ rb_ary_push((VALUE) keep, v32);
309
+ GetCArray(v32, bundle_ca[bi]);
310
+ ca_attach(bundle_ca[bi]);
311
+ bundle_codes[bi] = (int32_t *) bundle_ca[bi]->ptr;
312
+
313
+ int nb = (int) RARRAY_LEN(vbaxes);
314
+ if ( nb <= 0 || nb > src->ndim ) {
315
+ rb_raise(rb_eArgError, "axis_group_reduce: bad bundle axis count %d", nb);
316
+ }
317
+ bundle_nconsumed[bi] = nb;
318
+ bundle_k[bi] = k;
319
+ K_total *= k;
320
+ consumed_total += nb;
321
+
322
+ /* codes must be row-major over the consumed axes' dims */
323
+ ca_size_t expect = 1;
324
+ ca_size_t dims[CA_RANK_MAX];
325
+ for ( int j = 0; j < nb; j++ ) {
326
+ int a = NUM2INT(RARRAY_AREF(vbaxes, j));
327
+ if ( a < 0 || a >= src->ndim || ! is_group[a] ) {
328
+ rb_raise(rb_eArgError,
329
+ "axis_group_reduce: bundle axis %d not a group axis", a);
330
+ }
331
+ dims[j] = src->dim[a];
332
+ expect *= src->dim[a];
333
+ /* slot = position of source axis `a` within the ascending slab axes */
334
+ int slot = -1;
335
+ for ( long s = 0; s < ngroup; s++ ) {
336
+ if ( axes[s] == a ) { slot = (int) s; break; }
337
+ }
338
+ bundle_slot[bi][j] = slot; /* always found: a is a group axis */
339
+ }
340
+ if ( bundle_ca[bi]->elements != expect ) {
341
+ rb_raise(rb_eArgError,
342
+ "axis_group_reduce: codes length %lld != Π consumed dims %lld",
343
+ (long long) bundle_ca[bi]->elements, (long long) expect);
344
+ }
345
+ /* row-major code strides over the consumed axes (in given order) */
346
+ ca_size_t st = 1;
347
+ for ( int j = nb - 1; j >= 0; j-- ) {
348
+ bundle_cstride[bi][j] = st;
349
+ st *= dims[j];
350
+ }
351
+ }
352
+ if ( consumed_total != ngroup ) {
353
+ rb_raise(rb_eArgError,
354
+ "axis_group_reduce: Σ bundle ranks %ld != group axis count %ld",
355
+ consumed_total, ngroup);
356
+ }
357
+ /* place values: bundle 0 most significant (row-major over bundles) */
358
+ {
359
+ ca_size_t pv = 1;
360
+ for ( int bi = n_bundles - 1; bi >= 0; bi-- ) {
361
+ bundle_placeval[bi] = pv;
362
+ pv *= bundle_k[bi];
363
+ }
364
+ }
365
+
366
+ /* --- band layout + output shape [K_total, *band_dims] --- */
367
+ ca_size_t band = (group_prod > 0) ? (src->elements / group_prod) : 0;
368
+ ca_size_t odim[CA_RANK_MAX];
369
+ int8_t ondim = 1;
370
+ odim[0] = K_total;
371
+ for ( int8_t i = 0; i < src->ndim; i++ ) {
372
+ if ( ! is_group[i] ) odim[ondim++] = src->dim[i];
373
+ }
374
+ ca_size_t nout = K_total * band;
375
+
376
+ /* --- supported data type gate (before any allocation) --- */
377
+ ca = src;
378
+ switch ( src->data_type ) {
379
+ case CA_BOOLEAN: case CA_INT8: case CA_UINT8: case CA_INT16: case CA_UINT16:
380
+ case CA_INT32: case CA_UINT32: case CA_INT64: case CA_UINT64:
381
+ case CA_FLOAT32: case CA_FLOAT64: break;
382
+ default:
383
+ for ( int bi = 0; bi < n_bundles; bi++ ) ca_detach(bundle_ca[bi]);
384
+ rb_raise(rb_eRuntimeError,
385
+ "axis_group_reduce: unsupported source data_type %d",
386
+ src->data_type);
387
+ }
388
+
389
+ /* output data type per op */
390
+ int8_t out_dt = CA_FLOAT64;
391
+ if ( op == GR_COUNT ) out_dt = CA_INT64;
392
+ else if ( op == GR_MINADDR || op == GR_MAXADDR ) out_dt = CA_INT64;
393
+ else if ( op == GR_ALL || op == GR_ANY ) out_dt = CA_BOOLEAN;
394
+ else if ( op == GR_ACCUM ) out_dt = src->data_type;
395
+ VALUE vout = rb_carray_new(out_dt, ondim, odim, 0, NULL);
396
+ GetCArray(vout, co);
397
+
398
+ /* --- accumulator buffers (only those the op needs; all O(nout)) --- */
399
+ ca_size_t *cnt = ALLOC_N(ca_size_t, nout); MEMZERO(cnt, ca_size_t, nout);
400
+ double *sum = NULL, *sumsq = NULL, *prod = NULL, *mn = NULL, *mx = NULL;
401
+ ca_size_t *nz = NULL;
402
+ int64_t *mnaddr = NULL, *mxaddr = NULL; /* flat source addr of min / max */
403
+ ca_size_t *band_addr = NULL; /* raveled addr of each band cell */
404
+ if ( op == GR_SUM || op == GR_MEAN || op == GR_VARIANCE || op == GR_STDDEV ||
405
+ op == GR_VARIANCEP || op == GR_STDDEVP ) {
406
+ sum = ALLOC_N(double, nout); MEMZERO(sum, double, nout);
407
+ }
408
+ if ( op == GR_PROD ) {
409
+ prod = ALLOC_N(double, nout);
410
+ for ( ca_size_t o = 0; o < nout; o++ ) prod[o] = 1.0;
411
+ }
412
+ if ( op == GR_MIN || op == GR_MINADDR ) {
413
+ mn = ALLOC_N(double, nout);
414
+ for ( ca_size_t o = 0; o < nout; o++ ) mn[o] = HUGE_VAL;
415
+ }
416
+ if ( op == GR_MAX || op == GR_MAXADDR ) {
417
+ mx = ALLOC_N(double, nout);
418
+ for ( ca_size_t o = 0; o < nout; o++ ) mx[o] = -HUGE_VAL;
419
+ }
420
+ if ( op == GR_MINADDR ) { mnaddr = ALLOC_N(int64_t, nout); MEMZERO(mnaddr, int64_t, nout); }
421
+ if ( op == GR_MAXADDR ) { mxaddr = ALLOC_N(int64_t, nout); MEMZERO(mxaddr, int64_t, nout); }
422
+ if ( op == GR_ALL || op == GR_ANY ) {
423
+ nz = ALLOC_N(ca_size_t, nout); MEMZERO(nz, ca_size_t, nout);
424
+ }
425
+
426
+ /* min_addr / max_addr need the flat raveled source address of each cell.
427
+ Precompute the raveled stride per axis, the group-axis strides (paired with
428
+ the slab odometer sidx), and the band cell's base address per band flat
429
+ index b. Then addr(cell) = band_addr[b] + Σ sidx[s]*grstride[s]. */
430
+ ca_size_t rstride[CA_RANK_MAX], grstride[CA_RANK_MAX];
431
+ if ( op == GR_MINADDR || op == GR_MAXADDR ) {
432
+ rstride[src->ndim - 1] = 1;
433
+ for ( int8_t k = (int8_t)(src->ndim - 2); k >= 0; k-- )
434
+ rstride[k] = rstride[k+1] * src->dim[k+1];
435
+ for ( long s = 0; s < ngroup; s++ ) grstride[s] = rstride[axes[s]];
436
+ int band_axis[CA_RANK_MAX]; int nband_axes = 0;
437
+ for ( int8_t k = 0; k < src->ndim; k++ )
438
+ if ( ! is_group[k] ) band_axis[nband_axes++] = k;
439
+ band_addr = ALLOC_N(ca_size_t, band > 0 ? band : 1);
440
+ for ( ca_size_t bb = 0; bb < band; bb++ ) {
441
+ ca_size_t rem = bb, addr = 0;
442
+ for ( int j = nband_axes - 1; j >= 0; j-- ) { /* last band axis fastest */
443
+ ca_size_t d = src->dim[band_axis[j]];
444
+ addr += (rem % d) * rstride[band_axis[j]];
445
+ rem /= d;
446
+ }
447
+ band_addr[bb] = addr;
448
+ }
449
+ }
450
+
451
+ /* --- compute pass(es), native data type dispatch, no forced float64 cast ---
452
+ variance / stddev use a centred two-pass (= matches CArray's own
453
+ variance, avoids the one-pass sumsq cancellation that breaks ε-close
454
+ for small near-constant groups). Pass 1 fills sum + cnt; sum is then
455
+ overwritten in place with the per-group mean; pass 2 accumulates the
456
+ centred sum of squares. Every other op is a single pass. */
457
+ if ( op == GR_VARIANCE || op == GR_STDDEV ||
458
+ op == GR_VARIANCEP || op == GR_STDDEVP ) {
459
+ GROUP_DISPATCH( cnt[o] += 1; sum[o] += v; );
460
+ for ( ca_size_t o = 0; o < nout; o++ )
461
+ if ( cnt[o] > 0 ) sum[o] /= (double) cnt[o]; /* sum -> mean */
462
+ sumsq = ALLOC_N(double, nout); MEMZERO(sumsq, double, nout);
463
+ GROUP_DISPATCH( { double _d = v - sum[o]; sumsq[o] += _d * _d; } );
464
+ }
465
+ else if ( op == GR_MINADDR ) {
466
+ GROUP_DISPATCH(
467
+ cnt[o] += 1;
468
+ if ( v < mn[o] ) {
469
+ ca_size_t faddr = band_addr[b] + gaddr;
470
+ mn[o] = v; mnaddr[o] = (int64_t) faddr;
471
+ }
472
+ );
473
+ }
474
+ else if ( op == GR_MAXADDR ) {
475
+ GROUP_DISPATCH(
476
+ cnt[o] += 1;
477
+ if ( v > mx[o] ) {
478
+ ca_size_t faddr = band_addr[b] + gaddr;
479
+ mx[o] = v; mxaddr[o] = (int64_t) faddr;
480
+ }
481
+ );
482
+ }
483
+ else if ( op == GR_ACCUM ) {
484
+ MEMZERO(co->ptr, char, (size_t) nout * co->bytes);
485
+ switch ( ca->data_type ) {
486
+ case CA_BOOLEAN: GROUP_WALK(boolean8_t, GACC_XOR); break;
487
+ case CA_INT8: GROUP_WALK(int8_t, GACC_ADD(int8_t)); break;
488
+ case CA_UINT8: GROUP_WALK(uint8_t, GACC_ADD(uint8_t)); break;
489
+ case CA_INT16: GROUP_WALK(int16_t, GACC_ADD(int16_t)); break;
490
+ case CA_UINT16: GROUP_WALK(uint16_t, GACC_ADD(uint16_t)); break;
491
+ case CA_INT32: GROUP_WALK(int32_t, GACC_ADD(int32_t)); break;
492
+ case CA_UINT32: GROUP_WALK(uint32_t, GACC_ADD(uint32_t)); break;
493
+ case CA_INT64: GROUP_WALK(int64_t, GACC_ADD(int64_t)); break;
494
+ case CA_UINT64: GROUP_WALK(uint64_t, GACC_ADD(uint64_t)); break;
495
+ case CA_FLOAT32: GROUP_WALK(float, GACC_ADD(float)); break;
496
+ case CA_FLOAT64: GROUP_WALK(double, GACC_ADD(double)); break;
497
+ default: break;
498
+ }
499
+ }
500
+ else {
501
+ GROUP_DISPATCH(
502
+ cnt[o] += 1;
503
+ if ( sum ) sum[o] += v;
504
+ if ( prod ) prod[o] *= v;
505
+ if ( mn ) { if ( v < mn[o] ) mn[o] = v; }
506
+ if ( mx ) { if ( v > mx[o] ) mx[o] = v; }
507
+ if ( nz ) { if ( v != 0.0 ) nz[o] += 1; }
508
+ );
509
+ }
510
+
511
+ /* --- finalise into the output, UNDEF for empty groups --- */
512
+ boolean8_t *omask = NULL;
513
+ #define MARK_UNDEF(o) do { \
514
+ if ( ! omask ) { \
515
+ ca_create_mask(co); \
516
+ omask = (boolean8_t *) co->mask->ptr; \
517
+ } \
518
+ omask[o] = 1; \
519
+ } while (0)
520
+
521
+ /* Empty / all-masked groups follow the same zero-contribution contract as
522
+ CArray reductions (ERI, matching the categorical sibling): an identity-
523
+ bearing op returns its identity (sum 0, prod 1, count 0, all true, any
524
+ false), a ratio / extremum returns UNDEF. Sample variance/stddev: n==0
525
+ UNDEF, n==1 -> 0.0 (the n=1 contract), n>=2 the formula. */
526
+ if ( op == GR_COUNT ) {
527
+ int64_t *out = (int64_t *) co->ptr;
528
+ for ( ca_size_t o = 0; o < nout; o++ ) out[o] = (int64_t) cnt[o];
529
+ }
530
+ else if ( op == GR_MINADDR || op == GR_MAXADDR ) {
531
+ int64_t *out = (int64_t *) co->ptr;
532
+ int64_t *addr = ( op == GR_MINADDR ) ? mnaddr : mxaddr;
533
+ for ( ca_size_t o = 0; o < nout; o++ ) {
534
+ if ( cnt[o] == 0 ) { out[o] = 0; MARK_UNDEF(o); } /* empty -> UNDEF */
535
+ else out[o] = addr[o];
536
+ }
537
+ }
538
+ else if ( op == GR_ACCUM ) {
539
+ /* already folded in place, in the source's own type; empty groups hold 0 */
540
+ }
541
+ else if ( op == GR_ALL ) {
542
+ boolean8_t *out = (boolean8_t *) co->ptr; /* empty -> true (vacuous) */
543
+ for ( ca_size_t o = 0; o < nout; o++ )
544
+ out[o] = ( cnt[o] == 0 ) ? 1 : (nz[o] == cnt[o]);
545
+ }
546
+ else if ( op == GR_ANY ) {
547
+ boolean8_t *out = (boolean8_t *) co->ptr; /* empty -> false */
548
+ for ( ca_size_t o = 0; o < nout; o++ )
549
+ out[o] = ( cnt[o] == 0 ) ? 0 : (nz[o] > 0);
550
+ }
551
+ else {
552
+ double *out = (double *) co->ptr;
553
+ for ( ca_size_t o = 0; o < nout; o++ ) {
554
+ switch ( op ) {
555
+ case GR_SUM: out[o] = sum[o]; break; /* empty -> 0.0 (identity) */
556
+ case GR_PROD: out[o] = prod[o]; break; /* empty -> 1.0 (identity) */
557
+ case GR_MEAN:
558
+ if ( cnt[o] == 0 ) { out[o] = 0.0; MARK_UNDEF(o); }
559
+ else out[o] = sum[o] / (double) cnt[o];
560
+ break;
561
+ case GR_MIN:
562
+ if ( cnt[o] == 0 ) { out[o] = 0.0; MARK_UNDEF(o); } else out[o] = mn[o];
563
+ break;
564
+ case GR_MAX:
565
+ if ( cnt[o] == 0 ) { out[o] = 0.0; MARK_UNDEF(o); } else out[o] = mx[o];
566
+ break;
567
+ case GR_VARIANCE:
568
+ case GR_STDDEV:
569
+ if ( cnt[o] == 0 ) { out[o] = 0.0; MARK_UNDEF(o); }
570
+ else if ( cnt[o] == 1 ) { out[o] = 0.0; } /* n=1 contract */
571
+ else { double var = sumsq[o] / ( (double) cnt[o] - 1.0 );
572
+ out[o] = ( op == GR_STDDEV ) ? sqrt(var) : var; }
573
+ break;
574
+ case GR_VARIANCEP:
575
+ case GR_STDDEVP:
576
+ if ( cnt[o] == 0 ) { out[o] = 0.0; MARK_UNDEF(o); }
577
+ else { double varp = sumsq[o] / (double) cnt[o]; /* n>=1, n=1 -> 0.0 */
578
+ out[o] = ( op == GR_STDDEVP ) ? sqrt(varp) : varp; }
579
+ break;
580
+ }
581
+ }
582
+ }
583
+ #undef MARK_UNDEF
584
+
585
+ for ( int bi = 0; bi < n_bundles; bi++ ) ca_detach(bundle_ca[bi]);
586
+ xfree(cnt);
587
+ if ( sum ) xfree(sum);
588
+ if ( sumsq ) xfree(sumsq);
589
+ if ( prod ) xfree(prod);
590
+ if ( mn ) xfree(mn);
591
+ if ( mx ) xfree(mx);
592
+ if ( nz ) xfree(nz);
593
+ if ( mnaddr ) xfree(mnaddr);
594
+ if ( mxaddr ) xfree(mxaddr);
595
+ if ( band_addr ) xfree(band_addr);
596
+
597
+ RB_GC_GUARD(keep);
598
+ return vout;
599
+ }
600
+
601
+ /* =========================================================================
602
+ Grouped segment scan — the per-element-emit sibling of the reduce above.
603
+
604
+ Same fused single pass (CA_FOR_EACH_SLAB pins the grouped-axis union as the
605
+ slab, the band axes as the outer iter, composite group code per slab element
606
+ from the bundles), but instead of reading a dense accumulator at the end it
607
+ EMITS the running value per element into a source-shaped output:
608
+
609
+ reduce: acc[code] op= v then out[code, band] = acc[code]
610
+ scan: acc[code] op= v ; out[cell] = acc[code] (shape = source)
611
+
612
+ The group axis is NOT collapsed — output shape == source shape. Within each
613
+ band the slab elements are walked in odometer (row-major over the grouped
614
+ axes) order, so per group the accumulation runs in position order along the
615
+ grouped axes. The flat case (all axes grouped, one band) is the same walk
616
+ with a single slab = row-major appearance order, so flat and band-preserving
617
+ agree by construction.
618
+
619
+ The accumulator is dense [0, K_total) and is RESET at each band (each slab
620
+ is one band position, fully processed before the next), so peak extra memory
621
+ is O(K_total) — never O(input). Peak = output (= input size) + O(K_total).
622
+
623
+ Masked / excluded cells follow the core CArray scan: a slab element masked in
624
+ the source but with a valid group code does not update its group's total; it
625
+ HOLDS the current running value and its output is NOT masked (identity ops
626
+ always; extrema only once a member has been seen, else UNDEF — the empty
627
+ max/min reduction contract). A slab element excluded by the classifier (code
628
+ out of [0, k)) belongs to no group and stays UNDEF.
629
+ ------------------------------------------------------------------------- */
630
+
631
+ enum { GS_CUMSUM = 0, GS_CUMCOUNT, GS_CUMMAX, GS_CUMMIN, GS_CUMPROD };
632
+
633
+ /* Build the band-independent scatter plan for one slab. For each slab element
634
+ (in odometer order over the grouped axes) it records: the composite group
635
+ code (GW_SKIP if excluded by some bundle), the source data / mask byte
636
+ offsets from the slab pointers, and the group-relative raveled output address
637
+ (Σ sidx[s]*grstride[s]). These depend only on the slab odometer position,
638
+ not on the band, so every band reuses this one plan (built on the first
639
+ band). Keeps the bundle walk + odometer out of the per-(band × element) hot
640
+ path; peak metadata is O(slab_elements) = O(group_prod), never O(input). */
641
+ static void
642
+ group_scan_build_plan (ca_iter_state *st, boolean8_t *m,
643
+ int n_bundles, int32_t **bundle_codes,
644
+ ca_size_t *bundle_k, ca_size_t *bundle_placeval,
645
+ int *bundle_nconsumed, int (*bundle_slot)[CA_RANK_MAX],
646
+ ca_size_t (*bundle_cstride)[CA_RANK_MAX],
647
+ long ngroup, ca_size_t *grstride,
648
+ ca_size_t *sw_code, ca_size_t *sw_doff,
649
+ ca_size_t *sw_moff, ca_size_t *sw_addr)
650
+ {
651
+ int8_t sndim = st->slab_ndim;
652
+ ca_size_t SE = st->slab_elements;
653
+ ca_size_t sidx[CA_RANK_MAX];
654
+ for ( int8_t k = 0; k < sndim; k++ ) {
655
+ sidx[k] = 0;
656
+ }
657
+ for ( ca_size_t e = 0; e < SE; e++ ) {
658
+ int skip = 0;
659
+ ca_size_t code = 0;
660
+ for ( int bi = 0; bi < n_bundles; bi++ ) {
661
+ ca_size_t sub = 0;
662
+ for ( int j = 0; j < bundle_nconsumed[bi]; j++ ) {
663
+ sub += sidx[ bundle_slot[bi][j] ] * bundle_cstride[bi][j];
664
+ }
665
+ int32_t c = bundle_codes[bi][sub];
666
+ if ( c < 0 || (ca_size_t) c >= bundle_k[bi] ) {
667
+ skip = 1;
668
+ break;
669
+ }
670
+ code += (ca_size_t) c * bundle_placeval[bi];
671
+ }
672
+ ca_size_t doff = 0, moff = 0, gaddr = 0;
673
+ for ( int8_t k = 0; k < sndim; k++ ) {
674
+ doff += sidx[k] * st->slab_strides[k];
675
+ if ( m ) {
676
+ moff += sidx[k] * st->slab_mask_strides[k];
677
+ }
678
+ }
679
+ for ( long s = 0; s < ngroup; s++ ) {
680
+ gaddr += (ca_size_t) sidx[s] * grstride[s];
681
+ }
682
+ sw_code[e] = skip ? GW_SKIP : code;
683
+ sw_doff[e] = doff;
684
+ sw_moff[e] = moff;
685
+ sw_addr[e] = gaddr;
686
+ for ( int8_t k = sndim - 1; k >= 0; k-- ) { /* last slab axis ticks fastest */
687
+ if ( ++sidx[k] < st->slab_dims[k] ) {
688
+ break;
689
+ }
690
+ sidx[k] = 0;
691
+ }
692
+ }
693
+ }
694
+
695
+ /* GROUP_SCAN_WALK(T, EMIT, HOLD): the accumulate-in-double walk (cumsum /
696
+ cumprod / cumcount). One CA_FOR_EACH_SLAB pass monomorphised on the load
697
+ type T; the band-independent plan is built once (group_scan_build_plan) on
698
+ the first band. EMIT (a present cell) writes the running value at the cell's
699
+ raveled output address `addr`, with `v` (the element widened to double),
700
+ `code` (composite group code) and the accumulators accd / accn in scope.
701
+ These ops have an identity (0.0 sum / 1.0 prod / 0 count), so the accumulator
702
+ is valid before any member: a cell masked WITHIN its group holds the current
703
+ running value (HOLD) and its output is NOT masked — it just does not update
704
+ the accumulator, matching the core scan where a masked cell holds the running
705
+ acc. Only a cell excluded from every group (GW_SKIP) stays UNDEF. At each
706
+ band accd is reset to acc_init and accn to 0, so groups do not run across
707
+ bands. */
708
+ #define GROUP_SCAN_WALK(T, EMIT, HOLD) \
709
+ do { \
710
+ ca_iter_state st; \
711
+ char *p; \
712
+ boolean8_t *m; \
713
+ ca_size_t b = 0; \
714
+ int ready = 0; \
715
+ CA_FOR_EACH_SLAB(st, ca, axes, (int8_t) ngroup, CA_KERNEL_READ, p, m) { \
716
+ ca_size_t SE = st.slab_elements; \
717
+ if ( SE > 0 ) { \
718
+ if ( ! ready ) { \
719
+ group_scan_build_plan(&st, m, n_bundles, bundle_codes, bundle_k, \
720
+ bundle_placeval, bundle_nconsumed, \
721
+ bundle_slot, bundle_cstride, ngroup, \
722
+ grstride, sw_code, sw_doff, sw_moff, sw_addr);\
723
+ ready = 1; \
724
+ } \
725
+ for ( ca_size_t o = 0; o < K_total; o++ ) { \
726
+ accd[o] = acc_init; \
727
+ accn[o] = 0; \
728
+ } \
729
+ ca_size_t base = band_addr[b]; \
730
+ for ( ca_size_t e = 0; e < SE; e++ ) { \
731
+ ca_size_t code = sw_code[e]; \
732
+ ca_size_t addr = base + sw_addr[e]; \
733
+ if ( code == GW_SKIP ) { MARK_OUT_UNDEF(addr); continue; } \
734
+ if ( m && m[ sw_moff[e] ] ) { HOLD; continue; } \
735
+ double v = (double) ( *(T *)(p + sw_doff[e]) ); \
736
+ (void) v; \
737
+ EMIT; \
738
+ } \
739
+ } \
740
+ b++; \
741
+ } \
742
+ } while (0)
743
+
744
+ #define GROUP_SCAN_DISPATCH(EMIT, HOLD) \
745
+ switch ( ca->data_type ) { \
746
+ case CA_BOOLEAN: GROUP_SCAN_WALK(boolean8_t, EMIT, HOLD); break; \
747
+ case CA_INT8: GROUP_SCAN_WALK(int8_t, EMIT, HOLD); break; \
748
+ case CA_UINT8: GROUP_SCAN_WALK(uint8_t, EMIT, HOLD); break; \
749
+ case CA_INT16: GROUP_SCAN_WALK(int16_t, EMIT, HOLD); break; \
750
+ case CA_UINT16: GROUP_SCAN_WALK(uint16_t, EMIT, HOLD); break; \
751
+ case CA_INT32: GROUP_SCAN_WALK(int32_t, EMIT, HOLD); break; \
752
+ case CA_UINT32: GROUP_SCAN_WALK(uint32_t, EMIT, HOLD); break; \
753
+ case CA_INT64: GROUP_SCAN_WALK(int64_t, EMIT, HOLD); break; \
754
+ case CA_UINT64: GROUP_SCAN_WALK(uint64_t, EMIT, HOLD); break; \
755
+ case CA_FLOAT32: GROUP_SCAN_WALK(float, EMIT, HOLD); break; \
756
+ case CA_FLOAT64: GROUP_SCAN_WALK(double, EMIT, HOLD); break; \
757
+ default: break; \
758
+ }
759
+
760
+ /* GROUP_SCAN_EXTREMUM_WALK(T, CMP): running extremum (cummax / cummin). The
761
+ extremum keeps the source data type (its magnitude never grows), so it holds a
762
+ native T accumulator, not a widened double. The first member of a group
763
+ emits its own value: a per-group `seen` byte initialises the accumulator
764
+ lazily on first hit — no sentinel like HUGE_VAL, which an integer data type could
765
+ not represent. CMP is > for max, < for min: a later member replaces the
766
+ running extremum when `rv CMP acc`. A cell masked within its group holds the
767
+ current extremum once a member has been seen (output NOT masked, like sum);
768
+ before any member the extremum is undefined so the cell stays UNDEF (the
769
+ CArray reduction contract: empty max/min has no value — deliberately NOT the
770
+ type-min/-Inf init the core cumulative uses). GW_SKIP (excluded) stays
771
+ UNDEF. */
772
+ #define GROUP_SCAN_EXTREMUM_WALK(T, CMP) \
773
+ do { \
774
+ ca_iter_state st; \
775
+ char *p; \
776
+ boolean8_t *m; \
777
+ ca_size_t b = 0; \
778
+ int ready = 0; \
779
+ T *acce = ALLOC_N(T, K_total); \
780
+ T *outp = (T *) co->ptr; \
781
+ CA_FOR_EACH_SLAB(st, ca, axes, (int8_t) ngroup, CA_KERNEL_READ, p, m) { \
782
+ ca_size_t SE = st.slab_elements; \
783
+ if ( SE > 0 ) { \
784
+ if ( ! ready ) { \
785
+ group_scan_build_plan(&st, m, n_bundles, bundle_codes, bundle_k, \
786
+ bundle_placeval, bundle_nconsumed, \
787
+ bundle_slot, bundle_cstride, ngroup, \
788
+ grstride, sw_code, sw_doff, sw_moff, sw_addr);\
789
+ ready = 1; \
790
+ } \
791
+ for ( ca_size_t o = 0; o < K_total; o++ ) { \
792
+ seen[o] = 0; \
793
+ } \
794
+ ca_size_t base = band_addr[b]; \
795
+ for ( ca_size_t e = 0; e < SE; e++ ) { \
796
+ ca_size_t code = sw_code[e]; \
797
+ ca_size_t addr = base + sw_addr[e]; \
798
+ if ( code == GW_SKIP ) { MARK_OUT_UNDEF(addr); continue; } \
799
+ if ( m && m[ sw_moff[e] ] ) { \
800
+ if ( seen[code] ) { outp[addr] = acce[code]; } \
801
+ else { MARK_OUT_UNDEF(addr); } \
802
+ continue; \
803
+ } \
804
+ T rv = *(T *)(p + sw_doff[e]); \
805
+ if ( ! seen[code] ) { acce[code] = rv; seen[code] = 1; } \
806
+ else if ( rv CMP acce[code] ) { acce[code] = rv; } \
807
+ outp[addr] = acce[code]; \
808
+ } \
809
+ } \
810
+ b++; \
811
+ } \
812
+ xfree(acce); \
813
+ } while (0)
814
+
815
+ #define GROUP_SCAN_EXTREMUM_DISPATCH(CMP) \
816
+ switch ( ca->data_type ) { \
817
+ case CA_BOOLEAN: GROUP_SCAN_EXTREMUM_WALK(boolean8_t, CMP); break; \
818
+ case CA_INT8: GROUP_SCAN_EXTREMUM_WALK(int8_t, CMP); break; \
819
+ case CA_UINT8: GROUP_SCAN_EXTREMUM_WALK(uint8_t, CMP); break; \
820
+ case CA_INT16: GROUP_SCAN_EXTREMUM_WALK(int16_t, CMP); break; \
821
+ case CA_UINT16: GROUP_SCAN_EXTREMUM_WALK(uint16_t, CMP); break; \
822
+ case CA_INT32: GROUP_SCAN_EXTREMUM_WALK(int32_t, CMP); break; \
823
+ case CA_UINT32: GROUP_SCAN_EXTREMUM_WALK(uint32_t, CMP); break; \
824
+ case CA_INT64: GROUP_SCAN_EXTREMUM_WALK(int64_t, CMP); break; \
825
+ case CA_UINT64: GROUP_SCAN_EXTREMUM_WALK(uint64_t, CMP); break; \
826
+ case CA_FLOAT32: GROUP_SCAN_EXTREMUM_WALK(float, CMP); break; \
827
+ case CA_FLOAT64: GROUP_SCAN_EXTREMUM_WALK(double, CMP); break; \
828
+ default: break; \
829
+ }
830
+
831
+ /* GROUP_SCAN_OBJECT_WALK(EMIT, HOLD): the CA_OBJECT lane (source holds VALUEs).
832
+ A per-cell rb_funcall walk: cumcount is an int64 running count; the arithmetic
833
+ ops (cumsum / cumprod) accumulate a per-group VALUE from an identity seed (0 /
834
+ 1) and the extremum ops (cummax / cummin) from the first member itself, all
835
+ emitting into a CA_OBJECT output. EMIT sees `ev` (the source element VALUE)
836
+ and writes each running acc to the output cell immediately after computing it,
837
+ so between rb_funcall and the store no allocation runs and every live
838
+ accumulator is reachable from co (GC-safe).
839
+ A cell masked within its group runs HOLD, not the emit: cumcount holds the
840
+ running count and cumsum / cumprod hold their identity-seeded acc (0 / 1),
841
+ both unmasked (matching core object cumsum / cumprod); cummax / cummin hold
842
+ the running VALUE only once a member has been seen and stay UNDEF before that
843
+ (no identity for arbitrary objects). GW_SKIP (excluded) stays UNDEF. accn /
844
+ seen are reset at each band; acco carries the running VALUE. */
845
+ #define GROUP_SCAN_OBJECT_WALK(EMIT, HOLD) \
846
+ do { \
847
+ ca_iter_state st; \
848
+ char *p; \
849
+ boolean8_t *m; \
850
+ ca_size_t b = 0; \
851
+ int ready = 0; \
852
+ CA_FOR_EACH_SLAB(st, ca, axes, (int8_t) ngroup, CA_KERNEL_READ, p, m) { \
853
+ ca_size_t SE = st.slab_elements; \
854
+ if ( SE > 0 ) { \
855
+ if ( ! ready ) { \
856
+ group_scan_build_plan(&st, m, n_bundles, bundle_codes, bundle_k, \
857
+ bundle_placeval, bundle_nconsumed, \
858
+ bundle_slot, bundle_cstride, ngroup, \
859
+ grstride, sw_code, sw_doff, sw_moff, sw_addr);\
860
+ ready = 1; \
861
+ } \
862
+ for ( ca_size_t o = 0; o < K_total; o++ ) { \
863
+ seen[o] = 0; \
864
+ accn[o] = 0; \
865
+ } \
866
+ ca_size_t base = band_addr[b]; \
867
+ for ( ca_size_t e = 0; e < SE; e++ ) { \
868
+ ca_size_t code = sw_code[e]; \
869
+ ca_size_t addr = base + sw_addr[e]; \
870
+ if ( code == GW_SKIP ) { MARK_OUT_UNDEF(addr); continue; } \
871
+ if ( m && m[ sw_moff[e] ] ) { HOLD; continue; } \
872
+ VALUE ev = *(VALUE *)(p + sw_doff[e]); \
873
+ (void) ev; \
874
+ EMIT; \
875
+ } \
876
+ } \
877
+ b++; \
878
+ } \
879
+ } while (0)
880
+
881
+ static int
882
+ group_scan_op_code (VALUE vop)
883
+ {
884
+ ID id = SYM2ID(vop);
885
+ if ( id == rb_intern("cumsum") ) return GS_CUMSUM;
886
+ else if ( id == rb_intern("cumcount") ) return GS_CUMCOUNT;
887
+ else if ( id == rb_intern("cummax") ) return GS_CUMMAX;
888
+ else if ( id == rb_intern("cummin") ) return GS_CUMMIN;
889
+ else if ( id == rb_intern("cumprod") ) return GS_CUMPROD;
890
+ rb_raise(rb_eArgError, "axis_group_scan: unsupported op :%s", rb_id2name(id));
891
+ }
892
+
893
+ /* __axis_group_scan__(group_axes, bundles, op) — group-keyed segment scan of
894
+ * self over the union of `group_axes` into composite groups described by
895
+ * `bundles` (same argument shape as __axis_group_reduce__). Internal.
896
+ *
897
+ * Unlike the reduce the grouped axes are not collapsed: the result is a new
898
+ * CArray of the same shape as self, each cell holding the running statistic of
899
+ * its group up to and including that cell, in row-major position order along
900
+ * the grouped axes (per band).
901
+
902
+ op / output data type:
903
+ :cumsum -> float64, inclusive within-group running sum.
904
+ :cumprod -> float64, inclusive within-group running product (init 1.0;
905
+ float64 like cumsum since the product grows).
906
+ :cummax -> source data type, running within-group maximum (extrema do not
907
+ grow magnitude, so the data type is preserved; int stays int).
908
+ :cummin -> source data type, running within-group minimum.
909
+ :cumcount -> int64, 1-based within-group running count of present cells
910
+ (matching the core CArray#cumcount): the first present member
911
+ of a group emits 1, the next 2, ...
912
+ cumsum / cumprod keep float64 (matching the reduce siblings sum / prod);
913
+ integer-preserving sum / prod is a deliberate non-goal (overflow / data type
914
+ consistency), as on the reduce side. A CA_OBJECT source emits a CA_OBJECT
915
+ result for cumsum / cumprod / cummax / cummin (cumcount stays int64).
916
+
917
+ Masked-cell / excluded-cell policy (matches the core CArray scan): a cell
918
+ masked WITHIN its group holds its group's current running value and its
919
+ output is NOT masked — for cumsum / cumprod / cumcount (which have an
920
+ identity) always, for cummax / cummin (no identity) only once a member has
921
+ been seen; before any member the extremum is undefined so the cell stays
922
+ UNDEF. A cell excluded from every group (composite code out of [0,k)) belongs
923
+ to no group and stays UNDEF. */
924
+ static VALUE
925
+ rb_ca_axis_group_scan (VALUE self, VALUE vgaxes, VALUE vbundles, VALUE vop)
926
+ {
927
+ CArray *src, *ca, *co;
928
+ int op = group_scan_op_code(vop);
929
+
930
+ GetCArray(self, src);
931
+
932
+ if ( src->ndim <= 0 ) {
933
+ rb_raise(rb_eRuntimeError, "axis_group_scan: scalar source");
934
+ }
935
+
936
+ /* --- group (slab) axes (same validation as reduce) --- */
937
+ Check_Type(vgaxes, T_ARRAY);
938
+ long ngroup = RARRAY_LEN(vgaxes);
939
+ if ( ngroup <= 0 || ngroup > src->ndim ) {
940
+ rb_raise(rb_eArgError, "axis_group_scan: bad group axis count %ld", ngroup);
941
+ }
942
+ int8_t axes[CA_RANK_MAX];
943
+ char is_group[CA_RANK_MAX];
944
+ for ( int8_t i = 0; i < src->ndim; i++ ) is_group[i] = 0;
945
+ ca_size_t group_prod = 1;
946
+ for ( long i = 0; i < ngroup; i++ ) {
947
+ int a = NUM2INT(RARRAY_AREF(vgaxes, i));
948
+ if ( a < 0 || a >= src->ndim ) {
949
+ rb_raise(rb_eArgError, "axis_group_scan: group axis %d out of range", a);
950
+ }
951
+ if ( is_group[a] ) {
952
+ rb_raise(rb_eArgError, "axis_group_scan: duplicate group axis %d", a);
953
+ }
954
+ if ( i > 0 && a <= NUM2INT(RARRAY_AREF(vgaxes, i - 1)) ) {
955
+ rb_raise(rb_eArgError, "axis_group_scan: group axes must be ascending");
956
+ }
957
+ is_group[a] = 1;
958
+ axes[i] = (int8_t) a;
959
+ group_prod *= src->dim[a];
960
+ }
961
+
962
+ /* --- bundles: small per-group code tables (same as reduce) --- */
963
+ Check_Type(vbundles, T_ARRAY);
964
+ int n_bundles = (int) RARRAY_LEN(vbundles);
965
+ if ( n_bundles <= 0 || n_bundles > CA_RANK_MAX ) {
966
+ rb_raise(rb_eArgError, "axis_group_scan: bad bundle count %d", n_bundles);
967
+ }
968
+ int32_t *bundle_codes[CA_RANK_MAX];
969
+ ca_size_t bundle_k[CA_RANK_MAX];
970
+ ca_size_t bundle_placeval[CA_RANK_MAX];
971
+ int bundle_nconsumed[CA_RANK_MAX];
972
+ int bundle_slot[CA_RANK_MAX][CA_RANK_MAX];
973
+ ca_size_t bundle_cstride[CA_RANK_MAX][CA_RANK_MAX];
974
+ CArray *bundle_ca[CA_RANK_MAX];
975
+ volatile VALUE keep = rb_ary_new();
976
+
977
+ ca_size_t K_total = 1;
978
+ long consumed_total = 0;
979
+ for ( int bi = 0; bi < n_bundles; bi++ ) {
980
+ VALUE bundle = RARRAY_AREF(vbundles, bi);
981
+ Check_Type(bundle, T_ARRAY);
982
+ if ( RARRAY_LEN(bundle) != 3 ) {
983
+ rb_raise(rb_eArgError, "axis_group_scan: bundle must be [codes, k, axes]");
984
+ }
985
+ VALUE vcodes = RARRAY_AREF(bundle, 0);
986
+ ca_size_t k = (ca_size_t) NUM2LONG(RARRAY_AREF(bundle, 1));
987
+ VALUE vbaxes = RARRAY_AREF(bundle, 2);
988
+ Check_Type(vbaxes, T_ARRAY);
989
+ if ( k <= 0 ) {
990
+ rb_raise(rb_eArgError, "axis_group_scan: bundle k must be positive");
991
+ }
992
+
993
+ VALUE v32 = rb_ca_wrap_readonly(vcodes, INT2NUM(CA_INT32));
994
+ rb_ary_push((VALUE) keep, v32);
995
+ GetCArray(v32, bundle_ca[bi]);
996
+ ca_attach(bundle_ca[bi]);
997
+ bundle_codes[bi] = (int32_t *) bundle_ca[bi]->ptr;
998
+
999
+ int nb = (int) RARRAY_LEN(vbaxes);
1000
+ if ( nb <= 0 || nb > src->ndim ) {
1001
+ rb_raise(rb_eArgError, "axis_group_scan: bad bundle axis count %d", nb);
1002
+ }
1003
+ bundle_nconsumed[bi] = nb;
1004
+ bundle_k[bi] = k;
1005
+ K_total *= k;
1006
+ consumed_total += nb;
1007
+
1008
+ ca_size_t expect = 1;
1009
+ ca_size_t dims[CA_RANK_MAX];
1010
+ for ( int j = 0; j < nb; j++ ) {
1011
+ int a = NUM2INT(RARRAY_AREF(vbaxes, j));
1012
+ if ( a < 0 || a >= src->ndim || ! is_group[a] ) {
1013
+ rb_raise(rb_eArgError,
1014
+ "axis_group_scan: bundle axis %d not a group axis", a);
1015
+ }
1016
+ dims[j] = src->dim[a];
1017
+ expect *= src->dim[a];
1018
+ int slot = -1;
1019
+ for ( long s = 0; s < ngroup; s++ ) {
1020
+ if ( axes[s] == a ) { slot = (int) s; break; }
1021
+ }
1022
+ bundle_slot[bi][j] = slot;
1023
+ }
1024
+ if ( bundle_ca[bi]->elements != expect ) {
1025
+ rb_raise(rb_eArgError,
1026
+ "axis_group_scan: codes length %lld != Π consumed dims %lld",
1027
+ (long long) bundle_ca[bi]->elements, (long long) expect);
1028
+ }
1029
+ ca_size_t cs = 1;
1030
+ for ( int j = nb - 1; j >= 0; j-- ) {
1031
+ bundle_cstride[bi][j] = cs;
1032
+ cs *= dims[j];
1033
+ }
1034
+ }
1035
+ if ( consumed_total != ngroup ) {
1036
+ rb_raise(rb_eArgError,
1037
+ "axis_group_scan: Σ bundle ranks %ld != group axis count %ld",
1038
+ consumed_total, ngroup);
1039
+ }
1040
+ {
1041
+ ca_size_t pv = 1;
1042
+ for ( int bi = n_bundles - 1; bi >= 0; bi-- ) {
1043
+ bundle_placeval[bi] = pv;
1044
+ pv *= bundle_k[bi];
1045
+ }
1046
+ }
1047
+
1048
+ ca_size_t band = (group_prod > 0) ? (src->elements / group_prod) : 0;
1049
+
1050
+ /* --- supported data type gate (CA_OBJECT handled by its own lane below) --- */
1051
+ ca = src;
1052
+ switch ( src->data_type ) {
1053
+ case CA_BOOLEAN: case CA_INT8: case CA_UINT8: case CA_INT16: case CA_UINT16:
1054
+ case CA_INT32: case CA_UINT32: case CA_INT64: case CA_UINT64:
1055
+ case CA_FLOAT32: case CA_FLOAT64: case CA_OBJECT: break;
1056
+ default:
1057
+ for ( int bi = 0; bi < n_bundles; bi++ ) ca_detach(bundle_ca[bi]);
1058
+ rb_raise(rb_eRuntimeError,
1059
+ "axis_group_scan: unsupported source data_type %d",
1060
+ src->data_type);
1061
+ }
1062
+
1063
+ /* --- output data type per op: cumcount int64; cumsum / cumprod float64 (object
1064
+ source -> object); cummax / cummin preserve the source data type (object ->
1065
+ object). --- */
1066
+ int8_t out_dt;
1067
+ if ( op == GS_CUMCOUNT ) { out_dt = CA_INT64; }
1068
+ else if ( src->data_type == CA_OBJECT ) { out_dt = CA_OBJECT; }
1069
+ else if ( op == GS_CUMSUM || op == GS_CUMPROD ) { out_dt = CA_FLOAT64; }
1070
+ else { out_dt = src->data_type; }
1071
+
1072
+ /* --- source-shaped output --- */
1073
+ ca_size_t odim[CA_RANK_MAX];
1074
+ for ( int8_t i = 0; i < src->ndim; i++ ) odim[i] = src->dim[i];
1075
+ VALUE vout = rb_carray_new(out_dt, src->ndim, odim, 0, NULL);
1076
+ GetCArray(vout, co);
1077
+
1078
+ /* --- band raveled base address per band flat index (same construction as
1079
+ the reduce min_addr / max_addr path): addr(cell) = band_addr[b] +
1080
+ Σ sidx[s]*grstride[s].
1081
+
1082
+ CAREFUL: the walks below index the output by that raveled source address.
1083
+ This is only valid because `vout` is a freshly allocated contiguous
1084
+ entity of the source's shape, so output offset == source row-major
1085
+ raveled address. Handing a view here would scatter into wrong cells. --- */
1086
+ ca_size_t rstride[CA_RANK_MAX], grstride[CA_RANK_MAX];
1087
+ rstride[src->ndim - 1] = 1;
1088
+ for ( int8_t k = (int8_t)(src->ndim - 2); k >= 0; k-- )
1089
+ rstride[k] = rstride[k+1] * src->dim[k+1];
1090
+ for ( long s = 0; s < ngroup; s++ ) grstride[s] = rstride[axes[s]];
1091
+ int band_axis[CA_RANK_MAX]; int nband_axes = 0;
1092
+ for ( int8_t k = 0; k < src->ndim; k++ )
1093
+ if ( ! is_group[k] ) band_axis[nband_axes++] = k;
1094
+ ca_size_t *band_addr = ALLOC_N(ca_size_t, band > 0 ? band : 1);
1095
+ for ( ca_size_t bb = 0; bb < band; bb++ ) {
1096
+ ca_size_t rem = bb, addr = 0;
1097
+ for ( int j = nband_axes - 1; j >= 0; j-- ) {
1098
+ ca_size_t d = src->dim[band_axis[j]];
1099
+ addr += (rem % d) * rstride[band_axis[j]];
1100
+ rem /= d;
1101
+ }
1102
+ band_addr[bb] = addr;
1103
+ }
1104
+
1105
+ /* band-independent scatter plan (built once on the first band, sized to the
1106
+ slab = group_prod), + per-band dense [0, K_total) accumulators reset each
1107
+ band. accd (sum / prod), accn (running count) and seen (extremum first
1108
+ member / object identity init) are all tiny; acco (object running VALUE) is
1109
+ allocated only for the object arithmetic / extremum ops. */
1110
+ ca_size_t plan_n = (group_prod > 0) ? group_prod : 1;
1111
+ ca_size_t *sw_code = ALLOC_N(ca_size_t, plan_n);
1112
+ ca_size_t *sw_doff = ALLOC_N(ca_size_t, plan_n);
1113
+ ca_size_t *sw_moff = ALLOC_N(ca_size_t, plan_n);
1114
+ ca_size_t *sw_addr = ALLOC_N(ca_size_t, plan_n);
1115
+ double *accd = ALLOC_N(double, K_total);
1116
+ ca_size_t *accn = ALLOC_N(ca_size_t, K_total);
1117
+ char *seen = ALLOC_N(char, K_total);
1118
+ VALUE *acco = NULL;
1119
+ double acc_init = ( op == GS_CUMPROD ) ? 1.0 : 0.0;
1120
+
1121
+ boolean8_t *omask = NULL;
1122
+ #define MARK_OUT_UNDEF(o) do { \
1123
+ if ( ! omask ) { \
1124
+ ca_create_mask(co); \
1125
+ omask = (boolean8_t *) co->mask->ptr; \
1126
+ } \
1127
+ omask[o] = 1; \
1128
+ } while (0)
1129
+
1130
+ if ( src->data_type == CA_OBJECT ) {
1131
+ if ( op == GS_CUMCOUNT ) { /* int64 1-based running count */
1132
+ int64_t *outi = (int64_t *) co->ptr;
1133
+ GROUP_SCAN_OBJECT_WALK(
1134
+ accn[code] += 1; outi[addr] = (int64_t) accn[code];,
1135
+ outi[addr] = (int64_t) accn[code];
1136
+ );
1137
+ }
1138
+ else { /* object running VALUE */
1139
+ VALUE *outo = (VALUE *) co->ptr;
1140
+ acco = ALLOC_N(VALUE, K_total);
1141
+ switch ( op ) {
1142
+ case GS_CUMSUM:
1143
+ /* Identity 0 (matching the core object cumsum): a group's acc is lazily
1144
+ seeded to 0 (Fixnum, so 0 + ev promotes to ev's class), so a cell
1145
+ masked before any present member holds 0 unmasked and an all-masked
1146
+ group yields 0 everywhere. seen doubles as the per-band init flag. */
1147
+ GROUP_SCAN_OBJECT_WALK(
1148
+ if ( ! seen[code] ) { acco[code] = INT2FIX(0); seen[code] = 1; }
1149
+ acco[code] = rb_funcall(acco[code], rb_intern("+"), 1, ev);
1150
+ outo[addr] = acco[code];,
1151
+ if ( ! seen[code] ) { acco[code] = INT2FIX(0); seen[code] = 1; }
1152
+ outo[addr] = acco[code];
1153
+ );
1154
+ break;
1155
+ case GS_CUMPROD:
1156
+ /* Identity 1, same lazy-init as cumsum. */
1157
+ GROUP_SCAN_OBJECT_WALK(
1158
+ if ( ! seen[code] ) { acco[code] = INT2FIX(1); seen[code] = 1; }
1159
+ acco[code] = rb_funcall(acco[code], rb_intern("*"), 1, ev);
1160
+ outo[addr] = acco[code];,
1161
+ if ( ! seen[code] ) { acco[code] = INT2FIX(1); seen[code] = 1; }
1162
+ outo[addr] = acco[code];
1163
+ );
1164
+ break;
1165
+ case GS_CUMMAX:
1166
+ /* No identity: seed from the first member, hold only once seen; a cell
1167
+ masked before any member stays UNDEF (empty-max contract). */
1168
+ GROUP_SCAN_OBJECT_WALK(
1169
+ if ( ! seen[code] ) { acco[code] = ev; seen[code] = 1; }
1170
+ else if ( NUM2INT(rb_funcall(ev, rb_intern("<=>"), 1, acco[code])) > 0 ) {
1171
+ acco[code] = ev;
1172
+ }
1173
+ outo[addr] = acco[code];,
1174
+ if ( seen[code] ) { outo[addr] = acco[code]; }
1175
+ else { MARK_OUT_UNDEF(addr); }
1176
+ );
1177
+ break;
1178
+ case GS_CUMMIN:
1179
+ GROUP_SCAN_OBJECT_WALK(
1180
+ if ( ! seen[code] ) { acco[code] = ev; seen[code] = 1; }
1181
+ else if ( NUM2INT(rb_funcall(ev, rb_intern("<=>"), 1, acco[code])) < 0 ) {
1182
+ acco[code] = ev;
1183
+ }
1184
+ outo[addr] = acco[code];,
1185
+ if ( seen[code] ) { outo[addr] = acco[code]; }
1186
+ else { MARK_OUT_UNDEF(addr); }
1187
+ );
1188
+ break;
1189
+ }
1190
+ }
1191
+ }
1192
+ else { /* native data type dispatch */
1193
+ switch ( op ) {
1194
+ case GS_CUMSUM: {
1195
+ double *outd = (double *) co->ptr;
1196
+ GROUP_SCAN_DISPATCH( accd[code] += v; outd[addr] = accd[code];,
1197
+ outd[addr] = accd[code]; );
1198
+ break;
1199
+ }
1200
+ case GS_CUMPROD: {
1201
+ double *outd = (double *) co->ptr;
1202
+ GROUP_SCAN_DISPATCH( accd[code] *= v; outd[addr] = accd[code];,
1203
+ outd[addr] = accd[code]; );
1204
+ break;
1205
+ }
1206
+ case GS_CUMCOUNT: { /* 1-based running count */
1207
+ int64_t *outi = (int64_t *) co->ptr;
1208
+ GROUP_SCAN_DISPATCH( accn[code] += 1; outi[addr] = (int64_t) accn[code];,
1209
+ outi[addr] = (int64_t) accn[code]; );
1210
+ break;
1211
+ }
1212
+ case GS_CUMMAX:
1213
+ GROUP_SCAN_EXTREMUM_DISPATCH( > );
1214
+ break;
1215
+ case GS_CUMMIN:
1216
+ GROUP_SCAN_EXTREMUM_DISPATCH( < );
1217
+ break;
1218
+ }
1219
+ }
1220
+ #undef MARK_OUT_UNDEF
1221
+
1222
+ for ( int bi = 0; bi < n_bundles; bi++ ) ca_detach(bundle_ca[bi]);
1223
+ xfree(band_addr);
1224
+ xfree(sw_code);
1225
+ xfree(sw_doff);
1226
+ xfree(sw_moff);
1227
+ xfree(sw_addr);
1228
+ xfree(accd);
1229
+ xfree(accn);
1230
+ xfree(seen);
1231
+ if ( acco ) xfree(acco);
1232
+
1233
+ RB_GC_GUARD(keep);
1234
+ return vout;
1235
+ }
1236
+
1237
+ void
1238
+ Init_ca_axis_group (void)
1239
+ {
1240
+ rb_define_method(rb_cCArray, "__axis_group_reduce__",
1241
+ rb_ca_axis_group_reduce, 3);
1242
+ rb_define_method(rb_cCArray, "__axis_group_scan__",
1243
+ rb_ca_axis_group_scan, 3);
1244
+ }