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.
- checksums.yaml +4 -4
- data/.yardopts +6 -25
- data/CHANGELOG.md +338 -0
- data/{NEWS.md → CHANGELOG.v1.md} +3 -0
- data/LICENSE +1 -1
- data/README.md +120 -36
- data/carray.gemspec +32 -30
- data/ext/ca_array_pool.c +91 -0
- data/ext/ca_axis_descriptor.h +186 -0
- data/ext/ca_axis_dispatch.c +924 -0
- data/ext/ca_axis_group.c +1244 -0
- data/ext/ca_bincmp_dispatch.c +76 -0
- data/ext/ca_bincmp_dispatch.h +85 -0
- data/ext/ca_binop_dispatch.c +124 -0
- data/ext/ca_binop_dispatch.h +152 -0
- data/ext/ca_categorical_iterator.c +1375 -0
- data/ext/ca_compare.c +94 -0
- data/ext/ca_compare.h +26 -0
- data/ext/ca_composite_dispatch.c +414 -0
- data/ext/ca_composite_dispatch.h +116 -0
- data/ext/ca_for_buffer.h +96 -0
- data/ext/ca_for_each_element.h +239 -0
- data/ext/ca_group_iter.c +304 -0
- data/ext/ca_iter_substrate.h +325 -0
- data/ext/ca_kernel_iterator.c +4367 -0
- data/ext/ca_kernel_iterator.h +2596 -0
- data/ext/ca_moncmp_dispatch.c +37 -0
- data/ext/ca_moncmp_dispatch.h +62 -0
- data/ext/ca_monop_dispatch.c +200 -0
- data/ext/ca_monop_dispatch.h +235 -0
- data/ext/ca_obj_array.c +355 -359
- data/ext/ca_obj_bincmp.c +839 -0
- data/ext/ca_obj_binop.c +948 -0
- data/ext/ca_obj_bitarray.c +369 -164
- data/ext/ca_obj_bitfield.c +294 -234
- data/ext/ca_obj_block.c +189 -711
- data/ext/ca_obj_byte_swap.c +766 -0
- data/ext/ca_obj_const_string.c +967 -0
- data/ext/ca_obj_face.c +750 -0
- data/ext/ca_obj_face.h +279 -0
- data/ext/ca_obj_fake.c +239 -100
- data/ext/ca_obj_farray.c +54 -441
- data/ext/ca_obj_field.c +82 -529
- data/ext/ca_obj_fixlen_string.c +308 -0
- data/ext/ca_obj_grid.c +866 -440
- data/ext/ca_obj_meld.c +1039 -0
- data/ext/ca_obj_moncmp.c +588 -0
- data/ext/ca_obj_monop.c +1123 -0
- data/ext/ca_obj_object.c +866 -296
- data/ext/ca_obj_record.c +470 -0
- data/ext/ca_obj_reduce.c +97 -82
- data/ext/ca_obj_refer.c +593 -459
- data/ext/ca_obj_remap.c +475 -0
- data/ext/ca_obj_repeat.c +92 -477
- data/ext/ca_obj_roll.c +624 -0
- data/ext/ca_obj_select.c +344 -296
- data/ext/ca_obj_select_axis.c +1306 -0
- data/ext/ca_obj_shift.c +231 -793
- data/ext/ca_obj_source.c +78 -0
- data/ext/ca_obj_stack.c +1173 -0
- data/ext/ca_obj_stride.c +2584 -0
- data/ext/ca_obj_string.c +270 -0
- data/ext/ca_obj_tile.c +622 -0
- data/ext/ca_obj_time.c +548 -0
- data/ext/ca_obj_timedelta.c +437 -0
- data/ext/ca_obj_transpose.c +62 -516
- data/ext/ca_obj_triop.c +785 -0
- data/ext/ca_obj_window.c +1202 -565
- data/ext/ca_op_byte_swap.c +175 -0
- data/ext/ca_op_cmplx64.h +123 -0
- data/ext/ca_op_ipower.c +316 -0
- data/ext/ca_op_powi.h +88 -0
- data/ext/ca_sort_kernels.h +132 -0
- data/ext/ca_sweep_engine.c +473 -0
- data/ext/ca_sweep_engine.h +166 -0
- data/ext/ca_transform_common.c +235 -0
- data/ext/ca_triop_dispatch.c +55 -0
- data/ext/ca_triop_dispatch.h +62 -0
- data/ext/carray.h +810 -420
- data/ext/carray_access.c +873 -731
- data/ext/carray_attribute.c +98 -329
- data/ext/carray_bincount.c +255 -0
- data/ext/carray_broadcast.c +376 -0
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +2897 -874
- data/ext/carray_call_cfunc.h +313 -0
- data/ext/carray_cast.c +1264 -315
- data/ext/carray_cast_func.rb +81 -40
- data/ext/carray_class.c +53 -63
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +350 -346
- data/ext/carray_copy.c +168 -270
- data/ext/carray_core.c +1396 -206
- data/ext/carray_count.c +312 -0
- data/ext/carray_data_type.c +43 -19
- data/ext/carray_element.c +585 -213
- data/ext/carray_factorize.c +2542 -0
- data/ext/carray_generate.c +230 -559
- data/ext/carray_histogram.c +490 -0
- data/ext/carray_hold.c +228 -0
- data/ext/carray_index_classifier.c +1021 -0
- data/ext/carray_index_classifier.h +27 -0
- data/ext/carray_internal.h +136 -0
- data/ext/carray_kernels_bincmp.c +4446 -0
- data/ext/carray_kernels_binop.c +11001 -0
- data/ext/carray_kernels_init.c +1131 -0
- data/ext/carray_kernels_map.c +3467 -0
- data/ext/carray_kernels_moncmp.c +2097 -0
- data/ext/carray_kernels_monop.c +18313 -0
- data/ext/carray_kernels_reduce_aggregate.c +25837 -0
- data/ext/carray_kernels_reduce_boolean.c +330 -0
- data/ext/carray_kernels_reduce_cumulative.c +14593 -0
- data/ext/carray_kernels_reduce_extreme.c +16948 -0
- data/ext/carray_kernels_reduce_variance.c +3910 -0
- data/ext/carray_kernels_scan.c +3693 -0
- data/ext/carray_kernels_search.c +32138 -0
- data/ext/carray_kernels_sort.c +10626 -0
- data/ext/carray_kernels_triop.c +1392 -0
- data/ext/carray_lazy.c +737 -0
- data/ext/carray_loop.c +88 -200
- data/ext/carray_mask.c +853 -158
- data/ext/carray_math_kernel.h +120 -0
- data/ext/carray_mathfunc.c +10 -241
- data/ext/carray_median_percentile.c +1257 -0
- data/ext/carray_memory_view.c +1650 -0
- data/ext/carray_operator.c +1525 -320
- data/ext/carray_order.c +664 -1394
- data/ext/carray_partition.c +416 -0
- data/ext/carray_random.c +518 -0
- data/ext/carray_scatter.c +357 -0
- data/ext/carray_slab.c +1219 -0
- data/ext/carray_slab.h +84 -0
- data/ext/carray_sort.c +829 -0
- data/ext/carray_sort_kernel.c +620 -0
- data/ext/carray_struct.c +695 -0
- data/ext/carray_test.c +343 -229
- data/ext/carray_undef.c +34 -17
- data/ext/carray_utils.c +175 -74
- data/ext/extconf.rb +234 -55
- data/ext/mk_call_cfunc.rb +671 -0
- data/ext/mkkernel.rb +9096 -0
- data/ext/ruby_carray.c +211 -108
- data/ext/version.h +4 -14
- data/ext/version.rb +5 -13
- data/lib/carray/arrow_tensor.rb +401 -0
- data/lib/carray/attribute.rb +166 -0
- data/lib/carray/autoload_carray.rb +239 -0
- data/lib/carray/autoload_method_extension.rb +45 -0
- data/lib/carray/axis_group.rb +711 -0
- data/lib/carray/basics.rb +481 -0
- data/lib/carray/bincount_nd.rb +358 -0
- data/lib/carray/block_iterator.rb +614 -0
- data/lib/carray/boolean_reduce.rb +109 -0
- data/lib/carray/categorical.rb +561 -0
- data/lib/carray/categorical_iterator.rb +1084 -0
- data/lib/carray/complex.rb +150 -0
- data/lib/carray/conditional.rb +216 -0
- data/lib/carray/const_string.rb +228 -0
- data/lib/carray/construct.rb +160 -328
- data/lib/carray/core_extensions.rb +297 -0
- data/lib/carray/data_type_extension.rb +250 -0
- data/lib/carray/fixlen_string.rb +95 -0
- data/lib/carray/frame/concat.rb +132 -0
- data/lib/carray/frame/convert.rb +95 -0
- data/lib/carray/frame/csv_parser.rb +211 -0
- data/lib/carray/frame/frame.rb +642 -0
- data/lib/carray/frame/group.rb +186 -0
- data/lib/carray/frame/io.rb +321 -0
- data/lib/carray/frame/join.rb +248 -0
- data/lib/carray/frame/records.rb +99 -0
- data/lib/carray/frame/sort.rb +113 -0
- data/lib/carray/frame/verbs.rb +316 -0
- data/lib/carray/frame.rb +16 -0
- data/lib/carray/fuse_source.rb +123 -0
- data/lib/carray/fusion.rb +218 -0
- data/lib/carray/histogram.rb +512 -0
- data/lib/carray/inspect.rb +37 -26
- data/lib/carray/iterator.rb +58 -349
- data/lib/carray/lazy.rb +941 -0
- data/lib/carray/mask_gap_fill.rb +200 -0
- data/lib/carray/math.rb +78 -342
- data/lib/carray/meld_reduce.rb +289 -0
- data/lib/carray/methods/align_addr.rb +116 -0
- data/lib/carray/methods/bin.rb +128 -0
- data/lib/carray/methods/bincount.rb +87 -0
- data/lib/carray/methods/bit_string.rb +92 -0
- data/lib/carray/methods/broadcast.rb +63 -0
- data/lib/carray/methods/choose.rb +39 -0
- data/lib/carray/methods/composition.rb +280 -0
- data/lib/carray/methods/gather_nd.rb +206 -0
- data/lib/carray/methods/index.rb +39 -0
- data/lib/carray/methods/insert_block.rb +99 -0
- data/lib/carray/methods/is_in.rb +141 -0
- data/lib/carray/methods/join.rb +90 -0
- data/lib/carray/methods/locate_addr.rb +52 -0
- data/lib/carray/methods/mask_duplicates.rb +41 -0
- data/lib/carray/methods/meshgrid.rb +90 -0
- data/lib/carray/methods/mode.rb +126 -0
- data/lib/carray/methods/nunique.rb +46 -0
- data/lib/carray/methods/resize.rb +56 -0
- data/lib/carray/methods/snap.rb +161 -0
- data/lib/carray/methods/string_format.rb +57 -0
- data/lib/carray/methods/unique.rb +47 -0
- data/lib/carray/methods/value_counts.rb +71 -0
- data/lib/carray/mkmf.rb +124 -101
- data/lib/carray/runtime.rb +89 -0
- data/lib/carray/serialize.rb +478 -167
- data/lib/carray/slab_iterator.rb +305 -0
- data/lib/carray/stack.rb +291 -0
- data/lib/carray/string.rb +56 -180
- data/lib/carray/string_operation_extension.rb +289 -0
- data/lib/carray/struct.rb +335 -323
- data/lib/carray/struct_builder.rb +697 -0
- data/lib/carray/table.rb +41 -2
- data/lib/carray/time.rb +2654 -38
- data/lib/carray/window_iterator.rb +927 -0
- data/lib/carray.rb +55 -57
- data/yard-stubs/ca_obj_array.rb +385 -0
- data/yard-stubs/ca_obj_bitarray.rb +38 -0
- data/yard-stubs/ca_obj_bitfield.rb +43 -0
- data/yard-stubs/ca_obj_block.rb +73 -0
- data/yard-stubs/ca_obj_byte_swap.rb +56 -0
- data/yard-stubs/ca_obj_fake.rb +31 -0
- data/yard-stubs/ca_obj_farray.rb +32 -0
- data/yard-stubs/ca_obj_field.rb +45 -0
- data/yard-stubs/ca_obj_grid.rb +35 -0
- data/yard-stubs/ca_obj_refer.rb +72 -0
- data/yard-stubs/ca_obj_roll.rb +45 -0
- data/yard-stubs/ca_obj_shift.rb +43 -0
- data/yard-stubs/ca_obj_stride.rb +181 -0
- data/yard-stubs/ca_obj_tile.rb +29 -0
- data/yard-stubs/ca_obj_transpose.rb +40 -0
- data/yard-stubs/ca_obj_window.rb +49 -0
- data/yard-stubs/carray_access.rb +131 -0
- data/yard-stubs/carray_attribute.rb +246 -0
- data/yard-stubs/carray_broadcast.rb +37 -0
- data/yard-stubs/carray_cast.rb +489 -0
- data/yard-stubs/carray_class.rb +65 -0
- data/yard-stubs/carray_conversion.rb +76 -0
- data/yard-stubs/carray_copy.rb +79 -0
- data/yard-stubs/carray_core.rb +114 -0
- data/yard-stubs/carray_count.rb +79 -0
- data/yard-stubs/carray_element.rb +108 -0
- data/yard-stubs/carray_generate.rb +66 -0
- data/yard-stubs/carray_lazy.rb +23 -0
- data/yard-stubs/carray_loop.rb +140 -0
- data/yard-stubs/carray_mask.rb +259 -0
- data/yard-stubs/carray_math.rb +132 -0
- data/yard-stubs/carray_mathfunc.rb +45 -0
- data/yard-stubs/carray_median_percentile.rb +89 -0
- data/yard-stubs/carray_memory_view.rb +163 -0
- data/yard-stubs/carray_order.rb +312 -0
- data/yard-stubs/carray_random.rb +89 -0
- data/yard-stubs/carray_scatter.rb +106 -0
- data/yard-stubs/carray_slab.rb +57 -0
- data/yard-stubs/carray_sort.rb +163 -0
- data/yard-stubs/carray_test.rb +85 -0
- data/yard-stubs/carray_undef.rb +64 -0
- data/yard-stubs/carray_utils.rb +97 -0
- data/yard-stubs/ruby_carray.rb +193 -0
- metadata +220 -138
- data/Rakefile +0 -51
- data/TODO.md +0 -18
- data/ext/ca_iter_block.c +0 -257
- data/ext/ca_iter_dimension.c +0 -299
- data/ext/ca_iter_window.c +0 -214
- data/ext/ca_obj_mapping.c +0 -644
- data/ext/ca_obj_unbound_repeat.c +0 -529
- data/ext/carray_iterator.c +0 -641
- data/ext/carray_math.rb +0 -850
- data/ext/carray_numeric.c +0 -259
- data/ext/carray_sort_addr.c +0 -254
- data/ext/carray_stat.c +0 -2100
- data/ext/carray_stat_proc.rb +0 -1999
- data/ext/mkmath.rb +0 -741
- data/ext/ruby_ccomplex.c +0 -509
- data/ext/ruby_float_func.c +0 -86
- data/lib/carray/array.rb +0 -8
- data/lib/carray/autoload/autoload_base.rb +0 -19
- data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
- data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
- data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
- data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
- data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
- data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
- data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
- data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
- data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
- data/lib/carray/autoload/autoload_gem_random.rb +0 -8
- data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
- data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
- data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
- data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
- data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
- data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
- data/lib/carray/autoload/autoload_object_link.rb +0 -1
- data/lib/carray/autoload/autoload_object_pack.rb +0 -2
- data/lib/carray/autoload.rb +0 -141
- data/lib/carray/basic.rb +0 -191
- data/lib/carray/broadcast.rb +0 -101
- data/lib/carray/compose.rb +0 -315
- data/lib/carray/convert.rb +0 -115
- data/lib/carray/info.rb +0 -110
- data/lib/carray/io/imagemagick.rb +0 -235
- data/lib/carray/mask.rb +0 -102
- data/lib/carray/math/histogram.rb +0 -177
- data/lib/carray/math/recurrence.rb +0 -93
- data/lib/carray/object/ca_obj_iterator.rb +0 -50
- data/lib/carray/object/ca_obj_link.rb +0 -50
- data/lib/carray/object/ca_obj_pack.rb +0 -99
- data/lib/carray/obsolete.rb +0 -256
- data/lib/carray/ordering.rb +0 -181
- data/lib/carray/testing.rb +0 -51
- data/lib/carray/transform.rb +0 -109
- data/mailmap +0 -1
- data/misc/Methods.ja.md +0 -182
- data/misc/NOTE +0 -51
- data/spec/Classes/CABitfield_spec.rb +0 -58
- data/spec/Classes/CABlockIterator_spec.rb +0 -114
- data/spec/Classes/CABlock_spec.rb +0 -205
- data/spec/Classes/CAField_spec.rb +0 -39
- data/spec/Classes/CAGrid_spec.rb +0 -75
- data/spec/Classes/CAMap_spec.rb +0 -0
- data/spec/Classes/CAMapping_spec.rb +0 -105
- data/spec/Classes/CAObject_attribute_spec.rb +0 -33
- data/spec/Classes/CAObject_spec.rb +0 -33
- data/spec/Classes/CARefer_spec.rb +0 -93
- data/spec/Classes/CARepeat_spec.rb +0 -65
- data/spec/Classes/CASelect_spec.rb +0 -22
- data/spec/Classes/CAShift_spec.rb +0 -16
- data/spec/Classes/CAStruct_spec.rb +0 -71
- data/spec/Classes/CATranspose_spec.rb +0 -60
- data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
- data/spec/Classes/CAWindow_spec.rb +0 -54
- data/spec/Classes/CAWrap_spec.rb +0 -8
- data/spec/Classes/CArray_spec.rb +0 -184
- data/spec/Classes/CScalar_spec.rb +0 -55
- data/spec/Features/feature_130_spec.rb +0 -19
- data/spec/Features/feature_attributes_spec.rb +0 -280
- data/spec/Features/feature_boolean_spec.rb +0 -98
- data/spec/Features/feature_broadcast.rb +0 -116
- data/spec/Features/feature_cast_function.rb +0 -19
- data/spec/Features/feature_cast_spec.rb +0 -33
- data/spec/Features/feature_class_spec.rb +0 -84
- data/spec/Features/feature_complex_spec.rb +0 -42
- data/spec/Features/feature_composite_spec.rb +0 -124
- data/spec/Features/feature_convert_spec.rb +0 -46
- data/spec/Features/feature_copy_spec.rb +0 -123
- data/spec/Features/feature_creation_spec.rb +0 -84
- data/spec/Features/feature_element_spec.rb +0 -144
- data/spec/Features/feature_extream_spec.rb +0 -54
- data/spec/Features/feature_generate_spec.rb +0 -74
- data/spec/Features/feature_index_spec.rb +0 -69
- data/spec/Features/feature_mask_spec.rb +0 -580
- data/spec/Features/feature_math_spec.rb +0 -97
- data/spec/Features/feature_order_spec.rb +0 -146
- data/spec/Features/feature_ref_store_spec.rb +0 -209
- data/spec/Features/feature_serialization_spec.rb +0 -125
- data/spec/Features/feature_stat_spec.rb +0 -397
- data/spec/Features/feature_virtual_spec.rb +0 -48
- data/spec/Features/method_eq_spec.rb +0 -81
- data/spec/Features/method_is_nan_spec.rb +0 -12
- data/spec/Features/method_map_spec.rb +0 -54
- data/spec/Features/method_max_with.rb +0 -20
- data/spec/Features/method_min_with.rb +0 -19
- data/spec/Features/method_ne_spec.rb +0 -18
- data/spec/Features/method_project_spec.rb +0 -188
- data/spec/Features/method_ref_spec.rb +0 -27
- data/spec/Features/method_round_spec.rb +0 -11
- data/spec/Features/method_s_linspace_spec.rb +0 -48
- data/spec/Features/method_s_span_spec.rb +0 -14
- data/spec/Features/method_seq_spec.rb +0 -47
- data/spec/Features/method_sort_with.rb +0 -43
- data/spec/Features/method_sorted_with.rb +0 -29
- data/spec/Features/method_span_spec.rb +0 -42
- data/spec/Features/method_wrap_readonly_spec.rb +0 -43
- data/spec/UnitTest/test_CAVirtual.rb +0 -214
- data/spec/spec_all.rb +0 -10
- data/utils/ca_ase.rb +0 -21
- data/utils/ca_methods.rb +0 -15
- data/utils/cast_checker.rb +0 -30
- data/utils/convert_test.rb +0 -73
- data/utils/extract_yard.rb +0 -22
- data/utils/guess_shape.rb +0 -76
- data/utils/monkey_patch_methods.rb +0 -62
- 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
|