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