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,927 @@
1
+ # ----------------------------------------------------------------------------
2
+ #
3
+ # carray/window_iterator.rb
4
+ #
5
+ # CAWindowIterator — a rolling (sliding-window) reduction dispatcher, the
6
+ # Window member of the 3.0 iterator family (sibling of CASlabIterator /
7
+ # CACategoricalIterator). Where a slab iterator folds each non-overlapping
8
+ # slab, a window iterator folds an OVERLAPPING window centred on every anchor
9
+ # cell, so the output is shaped like the source (a rolling result) rather
10
+ # than an outer shape.
11
+ #
12
+ # sw = a.windows(-1..1) # width-3 window per anchor
13
+ # sw.mean # rolling mean, shaped like a
14
+ # sw.correlate(kernel) # bounded cross-correlation
15
+ # sw.convolve(kernel) # bounded convolution (kernel flipped)
16
+ #
17
+ # Engine: build a padded entity once (source copied into the interior, the
18
+ # margins filled by the boundary policy), take its `sliding_windows` view
19
+ # (a pure strided view over the padded buffer), and run a core reduction over
20
+ # the trailing window axes. One vectorized pass; the named reductions
21
+ # delegate straight to the core reduction, so their data type / mask / empty
22
+ # (ERI) / epsilon contracts are the core's, unchanged. This replaces the 2.0
23
+ # per-anchor C engine (ext/ca_iter_window.c, retired).
24
+ #
25
+ # Boundary policy is chosen at construction with `bounds:`:
26
+ #
27
+ # :skip (default) UNDEF margin; a window near the edge folds only its
28
+ # in-bounds cells (masked pad cells are skipped by the core
29
+ # reduction). Output is reference-shaped.
30
+ # :nearest edge-replicated margin (the nearest source cell extends
31
+ # outward). Output is reference-shaped, margin cells are real.
32
+ # :truncate no pad; only fully in-bounds anchors are produced, so the
33
+ # output shrinks to `N_i - w_i + 1` per axis. Zero-copy (the
34
+ # source's own sliding_windows view), the valid-convolution
35
+ # mode.
36
+ #
37
+ # How the boundary spectrum lands on the core reduction: with :skip the
38
+ # margin is UNDEF, so `min_count:` (require this many present cells) and
39
+ # `fill_value:` (replace an UNDEF result) — both passed straight through to
40
+ # the core reduction — express the full spectrum from "fold whatever is
41
+ # present" to "full windows only, edges filled". There is no window-specific
42
+ # strictness knob.
43
+ #
44
+ # The class name is kept from 2.0 (the concept — a window — is stable);
45
+ # this is the Ruby family member that supersedes the C engine. Loaded
46
+ # lazily via autoload from
47
+ # lib/carray/autoload_carray.rb the first time `a.windows(...)` is used.
48
+ #
49
+ # ----------------------------------------------------------------------------
50
+
51
+ require "carray"
52
+
53
+ # Rolling (sliding-window) reduction dispatcher — the Window member of the
54
+ # iterator family (sibling of `CASlabIterator` / {CABlockIterator} /
55
+ # `CACategoricalIterator`). It folds an overlapping window centred on every
56
+ # anchor cell, so the result is shaped like the source rather than reduced.
57
+ #
58
+ # Obtained from `CArray#windows`, not constructed directly.
59
+ #
60
+ # @example
61
+ # sw = a.windows(-1..1) # width-3 window per anchor
62
+ # sw.mean # rolling mean, shaped like a
63
+ # sw.correlate(kernel) # bounded cross-correlation
64
+ class CAWindowIterator < CAIterator
65
+
66
+ # @overload initialize(source, ranges, bounds: :skip, fill_value: nil)
67
+ # Builds a window iterator over `source` with a per-axis offset range.
68
+ # Each `ranges[i]` is a `lo..hi` giving the window's offset span around
69
+ # an anchor (`a.windows(-1..1)` is a centred width-3 window; `0..2` is
70
+ # forward-looking). `bounds:` selects the margin policy (`:skip` /
71
+ # `:nearest` / `:truncate`). `fill_value:` is a constant margin value
72
+ # (an escape for `:constant` padding); when given it overrides `:skip`.
73
+ #
74
+ # For backward compatibility `initialize(window_view)` accepts a CAWindow
75
+ # view (the old `CAWindowIterator.new(a.window(...))` form): the geometry
76
+ # (offset ranges, bounds, fill value) is read back from the view.
77
+ #
78
+ # @param source [CArray, CAWindow] the array to roll over, or a CAWindow
79
+ # view to read the geometry from.
80
+ # @param ranges [Array<Range>] per-axis offset ranges.
81
+ # @param bounds [Symbol] `:skip` / `:nearest` / `:truncate`.
82
+ # @param fill_value [Object, nil] constant margin value, overriding :skip.
83
+ def initialize (source, *ranges, bounds: :skip, fill_value: nil)
84
+ if source.is_a?(CArray) && source.obj_type == CA_OBJ_WINDOW
85
+ # Backward-compat: read geometry from a CAWindow view built by #window.
86
+ # start[i] = lo, dim[i] (window width) = w, so hi = lo + w - 1.
87
+ win = source
88
+ @source = win.parent
89
+ widths = win.count
90
+ @ranges = win.start.each_with_index.map { |lo, i| lo..(lo + widths[i] - 1) }
91
+ # The legacy #window default is FILL (constant), whose value is the
92
+ # view's fill_value; map that to a :constant margin.
93
+ @bounds = :constant
94
+ @fill_value = win.fill_value
95
+ else
96
+ @source = source
97
+ @ranges = ranges.flatten(0)
98
+ @bounds = bounds
99
+ @fill_value = fill_value
100
+ end
101
+
102
+ unless @ranges.size == @source.ndim
103
+ raise ArgumentError,
104
+ "windows: expected #{@source.ndim} ranges (one per axis), " \
105
+ "got #{@ranges.size}"
106
+ end
107
+
108
+ @sndim = @source.ndim
109
+ @widths = @ranges.map { |r| r.end - r.begin + 1 }
110
+ @lefts = @ranges.map { |r| [0, -r.begin].max } # left margin per axis
111
+ @rights = @ranges.map { |r| [0, r.end ].max } # right margin per axis
112
+
113
+ # A constant fill_value: overrides :skip (constant margin escape hatch).
114
+ @bounds = :constant if @fill_value != nil && @bounds == :skip
115
+
116
+ # Trailing window axes of the sliding_windows view: [ndim .. 2*ndim-1].
117
+ @window_axes = (@sndim...(2 * @sndim)).to_a
118
+
119
+ # Output iteration space (reference-shaped, except :truncate, which keeps
120
+ # only the anchors whose window lies wholly inside the source), and where
121
+ # each axis's first window starts in the buffer the windows are read from.
122
+ # A window that does not cover its own anchor -- `windows(1..2)`, the two
123
+ # cells after this one -- starts further along than the margin allows for,
124
+ # which is what @origins carries.
125
+ rshape = @source.shape
126
+ if @bounds == :truncate
127
+ first = @ranges.map { |r| [0, -r.begin].max }
128
+ last = @sndim.times.map { |i| [rshape[i] - 1, rshape[i] - 1 - @ranges[i].end].min }
129
+ @shape = @sndim.times.map { |i| [last[i] - first[i] + 1, 0].max }
130
+ @origins = @ranges.map { |r| [r.begin, 0].max }
131
+ else
132
+ @shape = rshape.dup
133
+ @origins = @sndim.times.map { |i| @lefts[i] + @ranges[i].begin }
134
+ end
135
+ @ndim = @shape.size
136
+ self
137
+ end
138
+
139
+ # @overload source
140
+ # Returns the array being rolled over.
141
+ # @return [CArray]
142
+ attr_reader :source
143
+
144
+ # @overload bounds
145
+ # Returns the boundary policy symbol.
146
+ # @return [Symbol]
147
+ attr_reader :bounds
148
+
149
+ # ---- padded entity + sliding_windows view -----------------------------
150
+ #
151
+ # The engine: pad the source once (:skip -> UNDEF, :nearest -> edge, :constant
152
+ # -> value; :truncate does not pad), then take a sliding_windows view. Both
153
+ # are memoised: every named reduction reuses the one pad + one view.
154
+
155
+ # @overload sliding_view
156
+ # Returns the sliding_windows view feeding the reductions. For :truncate
157
+ # this is the source's own view (zero-copy); otherwise it is the view over
158
+ # the padded entity.
159
+ # @return [CArray]
160
+ def sliding_view
161
+ @sliding_view ||= anchored_buffer.sliding_windows(*@widths)
162
+ end
163
+
164
+ private
165
+
166
+ # The stretch of the padded buffer the windows are taken from: it begins at
167
+ # the first window and holds one window per anchor. For a window that covers
168
+ # its own anchor and a boundary policy that pads, that is the whole buffer.
169
+ def anchored_buffer
170
+ buffer = padded_entity
171
+ wanted = @ndim.times.map { |k| @origins[k]...(@origins[k] + @shape[k] + @widths[k] - 1) }
172
+ return buffer if wanted.each_with_index.all? { |range, k|
173
+ range.begin.zero? && range.end == buffer.dim[k]
174
+ }
175
+ buffer[*wanted]
176
+ end
177
+
178
+ # Build the padded entity (or, for :truncate, the source itself). Memoised.
179
+ def padded_entity
180
+ @padded_entity ||=
181
+ if @bounds == :truncate
182
+ @source
183
+ else
184
+ pad_source(@source, @lefts, @rights, pad_mode, @fill_value)
185
+ end
186
+ end
187
+
188
+ def pad_mode
189
+ case @bounds
190
+ when :skip then :masked
191
+ when :nearest then :edge
192
+ when :constant then :constant
193
+ else
194
+ raise ArgumentError,
195
+ "windows: unknown bounds #{@bounds.inspect} " \
196
+ "(expected :skip, :nearest, :truncate)"
197
+ end
198
+ end
199
+
200
+ # Private pad helper: build a padded entity from `src` with per-axis
201
+ # (left, right) margins and one of three fill modes. Not a public
202
+ # CArray#pad -- window construction and convolution use it internally, the
203
+ # escape hatches yield from the padded entity, so users never call it. A
204
+ # standalone public CArray#pad is a possible future promotion.
205
+ #
206
+ # :constant margin cells set to `value` (0 when value is nil).
207
+ # :edge margin cells replicate the nearest source edge cell.
208
+ # :masked margin cells are UNDEF (masked); reductions skip them.
209
+ #
210
+ # One allocation, the source copied into the interior region, then the
211
+ # margins filled per mode.
212
+ def pad_source (src, lefts, rights, mode, value)
213
+ nd = src.ndim
214
+ shape = src.shape
215
+ pshape = nd.times.map { |i| shape[i] + lefts[i] + rights[i] }
216
+ pad = CArray.new(src.data_type, pshape)
217
+
218
+ # Fill the whole buffer first, then overwrite the interior with the source.
219
+ case mode
220
+ when :constant
221
+ pad[] = (value.nil? ? 0 : value)
222
+ when :masked
223
+ pad[] = UNDEF
224
+ when :edge
225
+ # Provisional fill; the edge margins are written below from the source.
226
+ pad[] = 0
227
+ end
228
+
229
+ interior = nd.times.map { |i| lefts[i]...(lefts[i] + shape[i]) }
230
+ pad[*interior] = src
231
+
232
+ replicate_edges(pad, lefts, rights, shape) if mode == :edge
233
+
234
+ pad
235
+ end
236
+
237
+ # Edge-replicate the margins of a padded buffer whose interior already holds
238
+ # the source. Per axis, the left margin rows copy the first interior row and
239
+ # the right margin rows copy the last; done axis by axis over the whole
240
+ # buffer (including corners, which pick up the replicated neighbours from an
241
+ # earlier axis, matching the standard edge-pad of corners).
242
+ def replicate_edges (pad, lefts, rights, shape)
243
+ pad.ndim.times do |ax|
244
+ lo = lefts[ax]
245
+ hi = lefts[ax] + shape[ax] - 1 # last interior index on this axis
246
+ if lefts[ax] > 0
247
+ src_sel = axis_selector(pad.ndim, ax, lo) # first interior row
248
+ lefts[ax].times do |m|
249
+ pad[*axis_selector(pad.ndim, ax, m)] = pad[*src_sel]
250
+ end
251
+ end
252
+ if rights[ax] > 0
253
+ src_sel = axis_selector(pad.ndim, ax, hi) # last interior row
254
+ (hi + 1...pad.shape[ax]).each do |m|
255
+ pad[*axis_selector(pad.ndim, ax, m)] = pad[*src_sel]
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ # How many of one axis's window offsets fall inside the source, per anchor.
262
+ # A window away from the edges holds all of them, so the vector is the window
263
+ # width everywhere but the two ends, and only the ends -- at most one window
264
+ # width of cells each -- are counted out.
265
+ def axis_cell_counts (k)
266
+ length = @shape[k]
267
+ first = @ranges[k].begin
268
+ last = @ranges[k].end
269
+ counts = CArray.int64(length)
270
+ counts[] = @widths[k]
271
+ truncated_at_the_start = [[-first, 0].max, length].min
272
+ truncated_at_the_end = [[length - last, 0].max, 0].max
273
+ indices = (0...truncated_at_the_start).to_a | (truncated_at_the_end...length).to_a
274
+ indices.each do |i|
275
+ low = [first, -i].max
276
+ high = [last, length - 1 - i].min
277
+ counts[i] = [high - low + 1, 0].max
278
+ end
279
+ counts
280
+ end
281
+
282
+ # An index list of length `nd` that is `nil` (full range) on every axis
283
+ # except `ax`, which is pinned to `k`.
284
+ def axis_selector (nd, ax, k)
285
+ sel = Array.new(nd)
286
+ sel[ax] = k
287
+ sel
288
+ end
289
+
290
+ public
291
+
292
+ # ---- named reductions (core delegation, drift zero) -------------------
293
+ #
294
+ # A per-window fold to one value over the trailing window axes is exactly a
295
+ # core per-axis reduction over those axes, so every reduction delegates to
296
+ # `sliding_view.<op>(axis: window_axes, ...)`. This inherits the core data type,
297
+ # mask, empty / all-masked (identity vs UNDEF) and epsilon-close contracts
298
+ # unchanged. `min_count:` / `fill_value:` pass straight to the core (the
299
+ # boundary strictness + result fill knobs).
300
+
301
+ # @overload sum(min_count: nil, fill_value: nil)
302
+ # Rolling sum, delegating to `sliding_view.sum(axis: window_axes)`.
303
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
304
+ # @overload accumulate(min_count: nil, fill_value: nil)
305
+ # Rolling sum in the source's own data type, wrapping at its width, as the
306
+ # core `accumulate` does. `sum` answers in the type the core promotes to
307
+ # (float64 for integers), which for a window over bytes moves eight times
308
+ # the bytes; this is the spelling for staying in the type when the window
309
+ # cannot overflow it.
310
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
311
+ # The rest are analogous: prod / mean / min / max, sample and population
312
+ # variance / stddev, all / any, fused minmax, and the window-local position
313
+ # min_index / max_index (index within the window axes).
314
+ [:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev, :all, :any,
315
+ :variancep, :stddevp, :minmax, :min_index, :max_index].each do |op|
316
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
317
+ def #{op} (min_count: nil, fill_value: nil)
318
+ folded = fold_by_offset(:#{op}, min_count, fill_value)
319
+ return folded unless folded.nil?
320
+ kw = {}
321
+ kw[:min_count] = min_count unless min_count.nil?
322
+ kw[:fill_value] = fill_value unless fill_value.nil?
323
+ sliding_view.#{op}(axis: @window_axes, **kw)
324
+ end
325
+ RUBY
326
+ end
327
+
328
+ # @overload min_addr
329
+ # Rolling flat SOURCE address of the window minimum — which source cell holds
330
+ # it, so `source.reshape(source.elements)[sw.min_addr]` are the window minima.
331
+ # Unlike `min_index` (the position within the window) this indexes back into
332
+ # the original array. The winner's source cell is the anchor plus its window
333
+ # offset; with `bounds: :nearest` a winning margin cell resolves to the edge
334
+ # source cell it replicates, and with `bounds: :constant` (or `fill_value:`)
335
+ # a winning margin cell has no source address and is a masked result.
336
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
337
+ def min_addr; window_winner_addr(:min_index); end
338
+
339
+ # @overload max_addr
340
+ # Rolling flat source address of the window maximum. See {#min_addr}.
341
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
342
+ def max_addr; window_winner_addr(:max_index); end
343
+
344
+ private
345
+
346
+ # ---- folding by offset instead of by anchor ---------------------------
347
+ #
348
+ # Delegating to `sliding_view.<op>(axis: window_axes)` folds the window axes,
349
+ # which are the innermost ones, so the core pays its per-fiber setup once per
350
+ # output cell -- about 10 ns, whatever the window holds. For a 3x3 window
351
+ # that setup is nine tenths of the time.
352
+ #
353
+ # The same fold can be run the other way round: walk the window offsets, and
354
+ # for each one add the whole shifted plane into an accumulator. Then the
355
+ # setup is paid once per offset rather than once per output cell, and every
356
+ # pass is a straight walk over contiguous memory. It costs one pass per
357
+ # offset, so it only pays while the window is small; the crossover moves with
358
+ # rank and data type, but stays above width 5 on every axis in every
359
+ # combination measured (see devel/PROPOSAL_WINDOW_OFFSET_ACCUMULATION.md).
360
+ #
361
+ # What is decided here is the order of the fold and which pad it reads; the
362
+ # arithmetic is the core's elementwise kernel, and the result data type is
363
+ # the core's answer for the same reduction (asked below, so it cannot drift).
364
+
365
+ # The in-place elementwise kernel that accumulates one offset, per operation.
366
+ # An operation absent here has no identity to accumulate from and always
367
+ # takes the delegating path.
368
+ OFFSET_FOLD = { :sum => :add!, :accumulate => :add!,
369
+ :prod => :mul!,
370
+ :min => :pmin!, :max => :pmax!,
371
+ :all => :and!, :any => :or! }.freeze
372
+
373
+ # Widest window this path takes on any one axis.
374
+ OFFSET_FOLD_MAX_WIDTH = 5
375
+
376
+ # Runs the fold by offset, or returns nil when this window is not one it can
377
+ # answer for -- in which case the caller delegates as before.
378
+ def fold_by_offset (op, min_count, fill_value = nil)
379
+ return nil unless op == :mean || OFFSET_FOLD.key?(op)
380
+ return nil if @widths.any? { |width| width > OFFSET_FOLD_MAX_WIDTH }
381
+ return nil if neutral_value(op).nil?
382
+
383
+ folded = op == :mean ? fold_mean : accumulate_offsets(op)
384
+ folded = mask_empty_windows(folded) if EMPTY_WINDOW_IS_UNDEFINED.include?(op)
385
+ folded = apply_min_count(folded, min_count)
386
+ # A `fill_value:` on the call replaces a result that came out undefined.
387
+ folded = folded.strip_mask(fill_value) if !fill_value.nil? && folded.has_mask?
388
+ folded
389
+ end
390
+
391
+ # The mean is the sum over the same offsets, divided by the number of cells
392
+ # each window folded. A window that folded nothing divides by one here and
393
+ # is masked out by {#mask_empty_windows} after.
394
+ def fold_mean
395
+ counts = window_cell_counts
396
+ divisor = counts
397
+ if counts.is_a?(CArray) && counts.min.zero?
398
+ divisor = counts.copy
399
+ divisor[counts.eq(0)] = 1
400
+ end
401
+ accumulate_offsets(:sum).to_type(offset_fold_data_type(:mean)) / divisor
402
+ end
403
+
404
+ # Walks the window offsets, accumulating each shifted plane into the result.
405
+ def accumulate_offsets (op, base = nil, kernel = nil, data_type = nil)
406
+ base ||= offset_fold_base(op)
407
+ kernel ||= OFFSET_FOLD.fetch(op)
408
+ data_type ||= offset_fold_data_type(op)
409
+ accumulator = nil
410
+ offset_grid.each do |offset|
411
+ plane = base[*@ndim.times.map { |k|
412
+ start = @origins[k] + offset[k]
413
+ start...(start + @shape[k])
414
+ }]
415
+ if accumulator.nil?
416
+ accumulator = CArray.new(data_type, @shape)
417
+ accumulator[] = plane
418
+ else
419
+ accumulator.send(kernel, plane)
420
+ end
421
+ end
422
+ accumulator
423
+ end
424
+
425
+ # These have no value to give for a window that folded nothing; the ones not
426
+ # listed answer with their identity, which the accumulation already holds.
427
+ EMPTY_WINDOW_IS_UNDEFINED = [:min, :max, :mean].freeze
428
+
429
+ def mask_empty_windows (folded)
430
+ return folded unless windows_can_be_empty?
431
+ folded[window_cell_counts.eq(0)] = UNDEF
432
+ folded
433
+ end
434
+
435
+ # Whether any window can come out holding nothing at all. A masked source
436
+ # can leave one empty anywhere. Without one, it takes a window that reaches
437
+ # past the array and does not cover its own anchor -- `windows(1..2)` at the
438
+ # far edge -- and the count on an axis falls away towards its ends, so the
439
+ # two ends are the only places to look.
440
+ def windows_can_be_empty?
441
+ return true if @source.has_mask?
442
+ return false if window_cell_counts.is_a?(Integer)
443
+ @ndim.times.any? do |k|
444
+ along = axis_cell_counts(k)
445
+ along[0].zero? || along[@shape[k] - 1].zero?
446
+ end
447
+ end
448
+
449
+ # `min_count` asks for a result only where the window held that many cells.
450
+ def apply_min_count (folded, min_count)
451
+ return folded if min_count.nil?
452
+ counts = window_cell_counts
453
+ if counts.is_a?(Integer)
454
+ folded[] = UNDEF if counts < min_count
455
+ else
456
+ folded[counts.lt(min_count)] = UNDEF
457
+ end
458
+ folded
459
+ end
460
+
461
+ # What stands in for a cell the fold must not see: a margin the boundary
462
+ # policy does not fill, or a masked cell of the source. For sum, prod, all
463
+ # and any that is the operation's identity. For min and max it is any value
464
+ # that cannot win, and the array's own extreme is one -- so no table of
465
+ # per-type limits is needed. Nil means there is none to be had (a source
466
+ # masked everywhere), and the caller delegates instead.
467
+ def neutral_value (op)
468
+ @neutral_value ||= {}
469
+ return @neutral_value[op] if @neutral_value.key?(op)
470
+ @neutral_value[op] =
471
+ case op
472
+ when :sum, :accumulate, :mean then 0
473
+ when :prod then 1
474
+ when :all then true
475
+ when :any then false
476
+ when :min then source_extreme(:max)
477
+ when :max then source_extreme(:min)
478
+ end
479
+ end
480
+
481
+ def source_extreme (op)
482
+ value = @source.send(op)
483
+ value.equal?(UNDEF) ? nil : value
484
+ end
485
+
486
+ # The buffer the offsets are read from: the source with its margins filled
487
+ # per the boundary policy, with any masked cell replaced by the neutral value
488
+ # -- an accumulation propagates a mask where the fold would skip it -- and in
489
+ # the type the result is accumulated in. Adding across two types runs a
490
+ # different kernel from adding within one, and how much slower that is
491
+ # depends on the pair, the working set and the compiler; converting once is
492
+ # one behaviour everywhere. It costs a buffer in the wider type, which for
493
+ # a `sum` over bytes is the one case where it does not pay.
494
+ def offset_fold_base (op)
495
+ @offset_fold_base ||= {}
496
+ @offset_fold_base[op] ||=
497
+ begin
498
+ padded =
499
+ if @source.has_mask?
500
+ pad_for(@source.strip_mask(neutral_value(op)), neutral_value(op))
501
+ elsif @bounds == :skip
502
+ pad_source(@source, @lefts, @rights, :constant, neutral_value(op))
503
+ else
504
+ padded_entity
505
+ end
506
+ wanted = offset_fold_data_type(op)
507
+ padded.data_type == wanted ? padded : padded.to_type(wanted)
508
+ end
509
+ end
510
+
511
+ # Pads `values` the way this window's boundary policy says, with `outside`
512
+ # standing in for the margin where the policy does not fill one.
513
+ def pad_for (values, outside)
514
+ case @bounds
515
+ when :truncate then values
516
+ when :nearest then pad_source(values, @lefts, @rights, :edge, nil)
517
+ when :constant then pad_source(values, @lefts, @rights, :constant, @fill_value)
518
+ else pad_source(values, @lefts, @rights, :constant, outside)
519
+ end
520
+ end
521
+
522
+ # How many cells each window holds. With an unmasked source this follows
523
+ # from the geometry: every boundary policy but `:skip` fills its margins with
524
+ # real values, so the count is the whole window and one Integer says it;
525
+ # `:skip` counts the in-bounds offsets, which is separable -- the count on
526
+ # one axis depends on that axis alone. A masked source has to be counted for
527
+ # real, by accumulating over the same offsets.
528
+ def window_cell_counts
529
+ return @window_cell_counts unless @window_cell_counts.nil?
530
+ @window_cell_counts =
531
+ if @source.has_mask?
532
+ accumulate_offsets(:sum, pad_for(@source.is_not_masked.int64, 0),
533
+ :add!, CA_INT64)
534
+ elsif @bounds != :skip
535
+ @widths.inject(:*)
536
+ else
537
+ counts = CArray.int64(*@shape)
538
+ counts[] = 1
539
+ @ndim.times do |k|
540
+ shape = Array.new(@ndim, 1)
541
+ shape[k] = @shape[k]
542
+ counts.mul!(axis_cell_counts(k).reshape(*shape))
543
+ end
544
+ counts
545
+ end
546
+ end
547
+
548
+ # The data type the delegating path would have produced, asked of the core
549
+ # itself so the two paths cannot disagree.
550
+ def offset_fold_data_type (op)
551
+ @offset_fold_data_type ||= {}
552
+ @offset_fold_data_type[op] ||=
553
+ CArray.new(@source.data_type, [1, 1]).send(op, axis: [1]).data_type
554
+ end
555
+
556
+ # Every offset within the window, as a list of per-axis positions into the
557
+ # padded buffer.
558
+ def offset_grid
559
+ @offset_grid ||=
560
+ @widths.map { |width| (0...width).to_a }
561
+ .inject { |grid, axis| grid.product(axis).map { |pair| Array(pair).flatten } }
562
+ .map { |offset| Array(offset) }
563
+ end
564
+
565
+ # Source address of the per-anchor winner. The window-local flat index
566
+ # (min_index / max_index) decomposes into per-axis window coordinates; the
567
+ # source coordinate on each axis is anchor + offset + window-coordinate (the
568
+ # offset is the range's begin for a padded margin, 0 for :truncate). A margin
569
+ # winner (coordinate out of bounds) resolves per the boundary policy:
570
+ # :nearest clamps to the edge source cell, :constant / :skip mask the result.
571
+ def window_winner_addr (idx_op)
572
+ mi = send(idx_op) # window-local flat index
573
+ n = @source.shape
574
+ wstride = Array.new(@sndim); acc = 1
575
+ (@sndim - 1).downto(0) { |i| wstride[i] = acc; acc *= @widths[i] }
576
+ sstride = Array.new(@sndim); acc = 1
577
+ (@sndim - 1).downto(0) { |i| sstride[i] = acc; acc *= n[i] }
578
+ lo = (@bounds == :truncate) ? @origins : @ranges.map(&:begin)
579
+ addr = CArray.int64(*@shape); addr[] = 0
580
+ oob = CArray.boolean(*@shape); oob[] = 0
581
+ (0...@sndim).each do |i|
582
+ w_i = (mi.int64 / wstride[i]) % @widths[i] # window coordinate on axis i
583
+ tshape = Array.new(@sndim, 1); tshape[i] = @shape[i]
584
+ o_i = CArray.int64(@shape[i]).seq!.reshape(*tshape) # anchor ramp (broadcasts)
585
+ coord = o_i + lo[i] + w_i
586
+ if @bounds == :nearest
587
+ coord[coord < 0] = 0
588
+ coord[coord >= n[i]] = n[i] - 1 # replicate the nearest edge cell
589
+ else
590
+ oob = oob | (coord < 0) | (coord >= n[i]) # margin winner -> no source cell
591
+ end
592
+ addr = addr + coord * sstride[i]
593
+ end
594
+ addr[oob] = UNDEF unless @bounds == :nearest
595
+ addr[mi.is_masked.eq(1)] = UNDEF if mi.has_mask?
596
+ addr
597
+ end
598
+
599
+ public
600
+
601
+ # @overload count(v = <none>)
602
+ # Rolling count over the window. No argument counts present (non-masked)
603
+ # cells (the effective tap count, which drops near a :skip edge);
604
+ # `count(UNDEF)` counts masked cells; `count(v)` counts cells equal to `v`.
605
+ # @return [CArray]
606
+ def count (*args)
607
+ return count_not_masked if args.empty?
608
+ # The sliding_windows view is a CAStride, so its #count is not shadowed;
609
+ # dispatch CArray#count explicitly anyway, matching the family regularity.
610
+ CArray.instance_method(:count).bind_call(sliding_view, *args, axis: @window_axes)
611
+ end
612
+
613
+ # @overload count_not_masked
614
+ # Rolling count of present (non-masked) cells -- the denominator of a
615
+ # renormalizing convolution.
616
+ # @return [CArray]
617
+ def count_not_masked
618
+ sliding_view.count_not_masked(axis: @window_axes)
619
+ end
620
+
621
+ # @overload count_masked
622
+ # Rolling count of masked cells.
623
+ # @return [CArray]
624
+ def count_masked
625
+ sliding_view.count_masked(axis: @window_axes)
626
+ end
627
+
628
+ # @overload elements
629
+ # Window cell count (structural, mask-independent): the constant window
630
+ # size `Π w_i`, shaped like the output.
631
+ # @return [CArray]
632
+ def elements
633
+ sz = @widths.inject(1) { |p, w| p * w }
634
+ # count_not_masked gives the correct output shape (and is not shadowed);
635
+ # overwrite with the constant window size.
636
+ out = sliding_view.count_not_masked(axis: @window_axes)
637
+ out[] = sz
638
+ out
639
+ end
640
+
641
+ # ---- correlate / convolve ---------------------------------------------
642
+ #
643
+ # A windowed weighted sum: `out[i] = Σ_j window[i][j] · kernel[j]`. The
644
+ # engine computes cross-correlation (kernel not flipped); convolution flips
645
+ # the kernel (one line). Both are exposed under their literal names because
646
+ # the flip convention splits by domain (signal processing flips, image / DL
647
+ # does not). For these, the constant margin default is 0.0 (the value a tap
648
+ # reaching outside the source contributes); override with a :constant fill.
649
+
650
+ # @overload correlate(kernel, min_count: nil, fill_value: nil)
651
+ # Rolling cross-correlation `out[i] = Σ_j a[i+j]·k[j]` (kernel not
652
+ # flipped). `kernel` has the shape of one window (`w_1 × ... × w_n`).
653
+ # @param kernel [CArray] weights shaped like a single window.
654
+ # @return [CArray]
655
+ def correlate (kernel, min_count: nil, fill_value: nil)
656
+ unless kernel.shape == @widths
657
+ raise ArgumentError,
658
+ "correlate: kernel shape #{kernel.shape.inspect} != " \
659
+ "window shape #{@widths.inspect}"
660
+ end
661
+ sv = sliding_view
662
+ # Explicit broadcast of the kernel over the anchor axes: reshape to
663
+ # 1 on every anchor axis, kernel width on every window axis (CArray forbids
664
+ # implicit cross-ndim broadcast, so the shape is made explicit).
665
+ kshape = ([1] * @sndim) + @widths
666
+ # The product routes operand promotion through the single-source binop
667
+ # coercion (result_type), so a float kernel over an int source promotes to
668
+ # float instead of truncating the weights. Do not coerce the kernel here.
669
+ prod = sv * kernel.reshape(*kshape)
670
+ kw = {}
671
+ kw[:min_count] = min_count unless min_count.nil?
672
+ kw[:fill_value] = fill_value unless fill_value.nil?
673
+ prod.sum(axis: @window_axes, **kw)
674
+ end
675
+
676
+ # @overload convolve(kernel, min_count: nil, fill_value: nil)
677
+ # Rolling convolution `out[i] = Σ_j a[i-j]·k[j]` (true convolution: the
678
+ # kernel is flipped on every window axis). Equals {#correlate} for a
679
+ # symmetric kernel.
680
+ # @param kernel [CArray] weights shaped like a single window.
681
+ # @return [CArray]
682
+ def convolve (kernel, min_count: nil, fill_value: nil)
683
+ correlate(reverse_all_axes(kernel), min_count: min_count, fill_value: fill_value)
684
+ end
685
+
686
+ private
687
+
688
+ # Reverse a kernel on every axis (`CArray#reverse` flips all axes at once).
689
+ def reverse_all_axes (kernel)
690
+ kernel.reverse
691
+ end
692
+
693
+ public
694
+
695
+ # ---- order statistics (median / percentile / quantile) ----------------
696
+ #
697
+ # Core per-axis order statistics take a single axis and do not accept a
698
+ # masked input, so the window mode is dispatched:
699
+ #
700
+ # single window axis + unmasked margin -> `sliding_view.op(axis: window_axis)`
701
+ # multi window axes + unmasked margin -> materialize the windows, flatten
702
+ # the window axes into one, single-
703
+ # axis core order-stat
704
+ # :skip (UNDEF margin) -> raise (core has no masked per-axis
705
+ # order-stat); guide to :nearest /
706
+ # :truncate
707
+ #
708
+ # When core gains masked per-axis order statistics (a tracked refactor), the
709
+ # :skip guard can be dropped and :skip served directly.
710
+
711
+ # @overload median
712
+ # Rolling median. Requires an unmasked margin (`bounds: :nearest` or
713
+ # `:truncate`); with the default `:skip` it raises.
714
+ # @return [CArray]
715
+ def median
716
+ order_stat { |view, axis| view.median(axis: axis) }
717
+ end
718
+
719
+ # @overload percentile(*pers)
720
+ # Rolling percentile(s). One argument returns one CArray, several return
721
+ # an array of CArrays (as `CArray#percentile`). Requires an unmasked
722
+ # margin.
723
+ # @return [CArray, Array<CArray>]
724
+ def percentile (*pers)
725
+ order_stat { |view, axis| view.percentile(*pers, axis: axis) }
726
+ end
727
+
728
+ # @overload quantile
729
+ # Rolling five-number summary `[min, Q1, median, Q3, max]` (five CArrays),
730
+ # as `CArray#quantile`. Requires an unmasked margin.
731
+ # @return [Array<CArray>]
732
+ def quantile
733
+ order_stat { |view, axis| view.quantile(axis: axis) }
734
+ end
735
+
736
+ private
737
+
738
+ # Drive an order statistic (yielded as `block.call(view, axis)`) through the
739
+ # single-axis / multi-axis / :skip-reject dispatch above.
740
+ def order_stat
741
+ if @bounds == :skip
742
+ raise ArgumentError,
743
+ "windowed order statistics need an unmasked margin; " \
744
+ "use bounds: :nearest (edge-extend) or bounds: :truncate (valid). " \
745
+ "For an UNDEF-margin median use reduce { |w| w.median } (slower)."
746
+ end
747
+ sv = sliding_view
748
+ if @window_axes.size == 1
749
+ yield sv, @window_axes[0]
750
+ else
751
+ # Materialize the overlapping windows, flatten the window axes into one,
752
+ # and run a single-axis core order-stat (vectorized; peak O(N·Πw)).
753
+ mat = sv.copy
754
+ wsize = @widths.inject(1) { |p, w| p * w }
755
+ flat = mat.reshape(*(@shape + [wsize]))
756
+ yield flat, @sndim # the flattened window axis
757
+ end
758
+ end
759
+
760
+ public
761
+
762
+ # ---- weighted (wsum / wmean) ------------------------------------------
763
+
764
+ # @overload wsum(weights)
765
+ # Rolling weighted sum, `weights` shaped like a single window.
766
+ # @return [CArray]
767
+ def wsum (weights)
768
+ weighted(weights) { |sv, w, axis| sv.wsum(w, axis: axis) }
769
+ end
770
+
771
+ # @overload wmean(weights)
772
+ # Rolling weighted mean, `weights` shaped like a single window.
773
+ # @return [CArray]
774
+ def wmean (weights)
775
+ weighted(weights) { |sv, w, axis| sv.wmean(w, axis: axis) }
776
+ end
777
+
778
+ private
779
+
780
+ def weighted (weights)
781
+ unless weights.shape == @widths
782
+ raise ArgumentError,
783
+ "wsum/wmean: weights shape #{weights.shape.inspect} != " \
784
+ "window shape #{@widths.inspect}"
785
+ end
786
+ sv = sliding_view
787
+ # Explicit broadcast of the per-window weights over the anchor axes, then
788
+ # grow to the full view shape (core wsum / wmean take a per-cell weight
789
+ # array shaped like the source, not the reduced-axis vector).
790
+ wshape = ([1] * @sndim) + @widths
791
+ wfull = weights.reshape(*wshape).broadcast_to(*sv.shape)
792
+ yield sv, wfull, @window_axes
793
+ end
794
+
795
+ public
796
+
797
+ # ---- generic iteration (escape hatch, slow) ---------------------------
798
+ #
799
+ # `each` yields every window (a per-window materialize -- slow, but the
800
+ # receptacle for statistics not in the named surface). `reduce` folds each
801
+ # window to one value (a custom rolling reduction), producing a reference-
802
+ # shaped output. `map` is defined only to raise NotImplementedError with an
803
+ # explanation: overlapping windows make an element-wise scatter-back
804
+ # ill-defined.
805
+
806
+ # @overload each { |window| ... }
807
+ # Yields each anchor's window as a CArray. Without a block, returns an
808
+ # Enumerator. Per-window materialize, slow; use a named reduction or
809
+ # {#convolve} for speed.
810
+ # @yieldparam window [CArray]
811
+ # @return [Enumerator, self]
812
+ def each
813
+ return to_enum(:each) unless block_given?
814
+ sv = sliding_view
815
+ nils = Array.new(@sndim, nil) # full window on the trailing axes
816
+ each_anchor_index { |idx| yield sv[*idx, *nils] }
817
+ self
818
+ end
819
+
820
+ # @overload reduce { |window| ... }
821
+ # Custom rolling reduction: the block receives each window (a CArray) and
822
+ # returns one value per anchor. The escape hatch for statistics not in the
823
+ # named surface.
824
+ # @yieldparam window [CArray]
825
+ # @return [CArray] reference-shaped (or shrunk, for :truncate)
826
+ # @overload reduce(init) { |acc, elem| ... }
827
+ # Per-window fiber fold: each window's cells are folded element by element
828
+ # starting from `init`.
829
+ # @param init [Object] initial accumulator.
830
+ # @return [CArray]
831
+ def reduce (*args, data_type: nil, &blk)
832
+ raise LocalJumpError, "no block given (yield)" unless blk
833
+ dt = data_type || CA_OBJECT
834
+ out = CArray.new(dt, @shape)
835
+ sv = sliding_view
836
+ nils = Array.new(@sndim, nil) # full window on the trailing axes
837
+ if args.empty?
838
+ each_anchor_index { |idx| out[*idx] = blk.call(sv[*idx, *nils]) }
839
+ else
840
+ init = args[0]
841
+ each_anchor_index do |idx|
842
+ acc = init
843
+ sv[*idx, *nils].each { |e| acc = blk.call(acc, e) }
844
+ out[*idx] = acc
845
+ end
846
+ end
847
+ out
848
+ end
849
+
850
+ # @overload map
851
+ # Not supported for a window iterator: overlapping windows share cells, so
852
+ # an element-wise transform has no well-defined scatter-back. Raises
853
+ # NotImplementedError; use {#reduce} for a custom per-window fold.
854
+ # @raise [NotImplementedError]
855
+ def map (*)
856
+ raise NotImplementedError,
857
+ "#{self.class} has no map: overlapping windows share cells, so an " \
858
+ "element-wise scatter-back is ill-defined; use reduce for a custom " \
859
+ "per-window fold."
860
+ end
861
+
862
+ # @overload sort_addr
863
+ # Not supported for a window iterator: a window's boundary cells are padding
864
+ # with no source address, and overlapping windows share cells, so a
865
+ # per-window sort returning source flat addresses is ill-defined. Raises
866
+ # NotImplementedError. (min_addr / max_addr are fine: the single winning
867
+ # cell of a window is a real source cell.)
868
+ # @raise [NotImplementedError]
869
+ def sort_addr (*)
870
+ raise NotImplementedError,
871
+ "#{self.class} has no sort_addr: padded boundary cells have no " \
872
+ "source address and overlapping windows share cells, so a per-window " \
873
+ "sort of source addresses is ill-defined."
874
+ end
875
+
876
+ # @overload cumsum
877
+ # @overload cumprod
878
+ # @overload cummax
879
+ # @overload cummin
880
+ # @overload cumcount
881
+ # Not supported for a window iterator: a segment scan writes a per-cell
882
+ # running statistic, which is single-valued only when each cell belongs to
883
+ # exactly one piece. Overlapping windows put a cell in many windows, so
884
+ # there is no single running value. Raises NotImplementedError, exactly as
885
+ # {#map} / {#sort_addr} do (min / max reductions stay available: a single
886
+ # winner is well-defined).
887
+ # @raise [NotImplementedError]
888
+ [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op|
889
+ define_method(op) do |*, **|
890
+ raise NotImplementedError,
891
+ "#{self.class} has no #{op}: a segment scan needs each cell to " \
892
+ "belong to exactly one piece, but overlapping windows share cells, " \
893
+ "so a per-cell running value is ill-defined; use reduce for a " \
894
+ "custom per-window fold."
895
+ end
896
+ end
897
+
898
+ private
899
+
900
+ # Yield every anchor index (the output iteration space) as an index Array of
901
+ # length `@ndim` (the anchor axes only; callers append the window nils).
902
+ def each_anchor_index
903
+ CArray.each_index(*@shape) do |*idx|
904
+ yield idx
905
+ end
906
+ end
907
+ end
908
+
909
+
910
+ class CArray
911
+ # @overload windows(*ranges, bounds: :skip, fill_value: nil)
912
+ # Returns a {CAWindowIterator} rolling a per-axis offset window over
913
+ # `self`. Each `ranges[i]` is a `lo..hi` offset span (`a.windows(-1..1)`
914
+ # is a centred width-3 window); `bounds:` selects the margin policy. With
915
+ # no ranges (`a.windows(a.window(...))` passing a CAWindow view) the
916
+ # geometry is read from the view for backward compatibility.
917
+ # @param ranges [Array<Range>] per-axis offset ranges.
918
+ # @param bounds [Symbol] `:skip` / `:nearest` / `:truncate`.
919
+ # @param fill_value [Object, nil] constant margin value.
920
+ # @return [CAWindowIterator]
921
+ def windows (*ranges, bounds: :skip, fill_value: nil)
922
+ if ranges.size == 1 && ranges[0].is_a?(CArray) && ranges[0].obj_type == CA_OBJ_WINDOW
923
+ return CAWindowIterator.new(ranges[0])
924
+ end
925
+ CAWindowIterator.new(self, *ranges, bounds: bounds, fill_value: fill_value)
926
+ end
927
+ end