carray 3.0.0 → 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 +2 -1
- data/CHANGELOG.md +325 -3
- data/{NEWS.md → CHANGELOG.v1.md} +1 -1
- data/README.md +32 -32
- data/carray.gemspec +12 -8
- data/ext/ca_axis_group.c +55 -19
- data/ext/ca_binop_dispatch.c +5 -6
- data/ext/ca_binop_dispatch.h +0 -7
- data/ext/ca_categorical_iterator.c +13 -13
- data/ext/ca_for_each_element.h +6 -8
- data/ext/ca_group_iter.c +1 -1
- data/ext/ca_kernel_iterator.c +69 -23
- data/ext/ca_kernel_iterator.h +3 -10
- data/ext/ca_obj_bincmp.c +57 -27
- data/ext/ca_obj_binop.c +86 -30
- data/ext/ca_obj_const_string.c +3 -1
- data/ext/ca_obj_face.c +80 -0
- data/ext/ca_obj_face.h +33 -1
- data/ext/ca_obj_fake.c +11 -0
- data/ext/ca_obj_fixlen_string.c +3 -1
- data/ext/ca_obj_grid.c +11 -3
- data/ext/ca_obj_meld.c +13 -8
- data/ext/ca_obj_moncmp.c +33 -14
- data/ext/ca_obj_monop.c +19 -7
- data/ext/ca_obj_object.c +99 -4
- data/ext/ca_obj_record.c +3 -1
- data/ext/ca_obj_refer.c +30 -6
- data/ext/ca_obj_roll.c +13 -5
- data/ext/ca_obj_select_axis.c +16 -6
- data/ext/ca_obj_shift.c +3 -3
- data/ext/ca_obj_stride.c +106 -23
- data/ext/ca_obj_string.c +3 -1
- data/ext/ca_obj_tile.c +13 -5
- data/ext/ca_obj_time.c +3 -1
- data/ext/ca_obj_timedelta.c +3 -1
- data/ext/ca_obj_transpose.c +1 -1
- data/ext/ca_obj_triop.c +62 -23
- data/ext/ca_obj_window.c +86 -17
- data/ext/ca_op_cmplx64.h +123 -0
- data/ext/ca_op_ipower.c +0 -3
- data/ext/ca_sort_kernels.h +5 -5
- data/ext/ca_sweep_engine.c +78 -35
- data/ext/ca_sweep_engine.h +13 -4
- data/ext/ca_transform_common.c +7 -0
- data/ext/carray.h +127 -130
- data/ext/carray_access.c +56 -34
- data/ext/carray_bincount.c +8 -8
- data/ext/carray_broadcast.c +99 -6
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +1491 -0
- data/ext/carray_call_cfunc.h +153 -0
- data/ext/carray_cast.c +69 -31
- data/ext/carray_conversion.c +22 -22
- data/ext/carray_copy.c +12 -2
- data/ext/carray_core.c +57 -10
- data/ext/carray_count.c +1 -1
- data/ext/carray_element.c +7 -7
- data/ext/carray_factorize.c +28 -28
- data/ext/carray_hold.c +1 -1
- data/ext/carray_index_classifier.c +7 -21
- data/ext/carray_internal.h +19 -3
- data/ext/carray_kernels_bincmp.c +1 -0
- data/ext/carray_kernels_binop.c +320 -298
- data/ext/carray_kernels_init.c +1095 -0
- data/ext/carray_kernels_map.c +1 -0
- data/ext/carray_kernels_moncmp.c +1 -0
- data/ext/carray_kernels_monop.c +412 -411
- data/ext/carray_kernels_reduce_aggregate.c +88 -87
- data/ext/carray_kernels_reduce_boolean.c +1 -0
- data/ext/carray_kernels_reduce_cumulative.c +54 -53
- data/ext/carray_kernels_reduce_extreme.c +51 -50
- data/ext/carray_kernels_reduce_variance.c +1 -0
- data/ext/carray_kernels_scan.c +1 -0
- data/ext/carray_kernels_search.c +93 -92
- data/ext/carray_kernels_sort.c +1 -0
- data/ext/carray_kernels_triop.c +1 -0
- data/ext/carray_lazy.c +175 -5
- data/ext/carray_mask.c +6 -5
- data/ext/carray_median_percentile.c +1 -1
- data/ext/carray_memory_view.c +46 -21
- data/ext/carray_operator.c +46 -49
- data/ext/carray_partition.c +4 -4
- data/ext/carray_random.c +7 -7
- data/ext/carray_scatter.c +1 -1
- data/ext/carray_sort.c +3 -3
- data/ext/carray_sort_kernel.c +10 -10
- data/ext/carray_test.c +1 -1
- data/ext/extconf.rb +18 -0
- data/ext/mk_call_cfunc.rb +243 -52
- data/ext/mkkernel.rb +354 -100
- data/ext/ruby_carray.c +18 -16
- data/ext/version.h +4 -4
- data/lib/carray/autoload_carray.rb +19 -0
- data/lib/carray/autoload_method_extension.rb +2 -1
- data/lib/carray/axis_group.rb +7 -7
- data/lib/carray/basics.rb +59 -59
- data/lib/carray/bincount_nd.rb +12 -12
- data/lib/carray/block_iterator.rb +24 -14
- data/lib/carray/categorical.rb +4 -4
- data/lib/carray/categorical_iterator.rb +98 -76
- data/lib/carray/conditional.rb +14 -14
- data/lib/carray/construct.rb +21 -0
- data/lib/carray/core_extensions.rb +62 -5
- data/lib/carray/data_type_extension.rb +18 -1
- data/lib/carray/frame/concat.rb +3 -3
- data/lib/carray/frame/convert.rb +1 -1
- data/lib/carray/frame/frame.rb +4 -11
- data/lib/carray/frame/io.rb +159 -2
- data/lib/carray/frame/sort.rb +1 -1
- data/lib/carray/frame/verbs.rb +18 -1
- data/lib/carray/fuse_source.rb +123 -0
- data/lib/carray/fusion.rb +218 -0
- data/lib/carray/histogram.rb +16 -16
- data/lib/carray/inspect.rb +1 -7
- data/lib/carray/iterator.rb +4 -3
- data/lib/carray/lazy.rb +125 -73
- data/lib/carray/meld_reduce.rb +2 -2
- data/lib/carray/methods/align_addr.rb +1 -1
- data/lib/carray/methods/composition.rb +1 -1
- data/lib/carray/methods/is_in.rb +12 -12
- data/lib/carray/methods/locate_addr.rb +6 -1
- data/lib/carray/methods/mask_duplicates.rb +1 -1
- data/lib/carray/methods/meshgrid.rb +4 -5
- data/lib/carray/methods/mode.rb +2 -2
- data/lib/carray/methods/nunique.rb +1 -1
- data/lib/carray/methods/snap.rb +7 -2
- data/lib/carray/methods/unique.rb +3 -3
- data/lib/carray/methods/value_counts.rb +2 -2
- data/lib/carray/runtime.rb +0 -19
- data/lib/carray/slab_iterator.rb +20 -7
- data/lib/carray/string_operation_extension.rb +5 -5
- data/lib/carray/time.rb +890 -491
- data/lib/carray/window_iterator.rb +285 -13
- data/lib/carray.rb +5 -5
- 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 +59 -10
- data/ext/ca_obj_unbound_repeat.c +0 -496
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# Reading `CArray.fuse { a + b * c }`.
|
|
3
|
+
#
|
|
4
|
+
# The block is not called. Its `a` is the array itself, so calling it would
|
|
5
|
+
# evaluate the expression eagerly -- which is the thing fuse exists to avoid.
|
|
6
|
+
# The source is read instead, every name that holds a CArray is given `.lazy`,
|
|
7
|
+
# and the result is evaluated back in the block's own binding, so `self`,
|
|
8
|
+
# instance variables, methods and constants are what they were.
|
|
9
|
+
#
|
|
10
|
+
# Ruby has no macro, so the alternative was to pass the arrays in and take
|
|
11
|
+
# shadows back -- `fuse(a, b) { |x, y| ... }` -- which names each of them
|
|
12
|
+
# twice. Julia writes `@.` for the same reason and does the same thing to
|
|
13
|
+
# the expression underneath.
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
require "prism"
|
|
17
|
+
|
|
18
|
+
class CArray
|
|
19
|
+
|
|
20
|
+
module FuseSource
|
|
21
|
+
|
|
22
|
+
# Runtime coercion, so the rewrite never has to work out what a name
|
|
23
|
+
# holds: anything that is not an array passes through untouched.
|
|
24
|
+
def self.shadow (value)
|
|
25
|
+
value.is_a?(CArray) ? value.lazy : value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.evaluate (block)
|
|
29
|
+
result = eval(rewrite(body_source(block)), block.binding,
|
|
30
|
+
*block.source_location)
|
|
31
|
+
# An expression that is just an array is that array; the shadow put
|
|
32
|
+
# around it has nothing to fuse.
|
|
33
|
+
result.is_a?(CALazyMarker) ? result.parent : result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# -- the block's own text ---------------------------------------------
|
|
37
|
+
|
|
38
|
+
def self.body_source (block)
|
|
39
|
+
text = extract(block)
|
|
40
|
+
wrapped = "proc " + text
|
|
41
|
+
node = Prism.parse(wrapped).value
|
|
42
|
+
.breadth_first_search { |n| n.is_a?(Prism::BlockNode) }
|
|
43
|
+
inner = node && node.body
|
|
44
|
+
unless inner
|
|
45
|
+
raise ArgumentError,
|
|
46
|
+
"CArray.fuse could not read an expression out of this block"
|
|
47
|
+
end
|
|
48
|
+
wrapped.byteslice(inner.location.start_offset...inner.location.end_offset)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.extract (block)
|
|
52
|
+
sequence = RubyVM::InstructionSequence.of(block) rescue nil
|
|
53
|
+
location = sequence && sequence.to_a[4][:code_location]
|
|
54
|
+
path = sequence && (sequence.absolute_path || sequence.path)
|
|
55
|
+
unless location && path && File.readable?(path)
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"CArray.fuse cannot read this block's source (defined in irb, " \
|
|
58
|
+
"eval, or a file that is no longer there). Write `.lazy` on " \
|
|
59
|
+
"the operands instead: `a.lazy + b.lazy`."
|
|
60
|
+
end
|
|
61
|
+
lines = File.readlines(path)
|
|
62
|
+
first_line, first_column, last_line, last_column = location
|
|
63
|
+
# The columns count bytes, not characters, so a line with anything
|
|
64
|
+
# multi-byte on it slices in the wrong place unless this does too.
|
|
65
|
+
if first_line == last_line
|
|
66
|
+
lines[first_line - 1].byteslice(first_column...last_column)
|
|
67
|
+
else
|
|
68
|
+
[lines[first_line - 1].byteslice(first_column..),
|
|
69
|
+
*lines[first_line...(last_line - 1)],
|
|
70
|
+
lines[last_line - 1].byteslice(0...last_column)].join
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# -- the rewrite -------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
# The leaves are the names being read. Everything else keeps its shape:
|
|
77
|
+
# calls are inserted around leaves and the expression they sit in is
|
|
78
|
+
# left alone.
|
|
79
|
+
class Leaves < Prism::Visitor
|
|
80
|
+
attr_reader :spots
|
|
81
|
+
|
|
82
|
+
def initialize
|
|
83
|
+
@spots = []
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def visit_local_variable_read_node (node) = mark(node)
|
|
87
|
+
def visit_instance_variable_read_node (node) = mark(node)
|
|
88
|
+
def visit_constant_read_node (node) = mark(node)
|
|
89
|
+
|
|
90
|
+
# `Math::PI` is one name, not `Math` with something after it.
|
|
91
|
+
def visit_constant_path_node (node)
|
|
92
|
+
mark(node)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def visit_call_node (node)
|
|
96
|
+
if node.name == :[] || node.name == :[]=
|
|
97
|
+
# An index is a position, not a value to fuse: `a[i]` shadows `a`
|
|
98
|
+
# and leaves `i` alone.
|
|
99
|
+
visit(node.receiver)
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
mark(node) if node.receiver.nil? && node.arguments.nil? && node.block.nil?
|
|
103
|
+
super
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def mark (node)
|
|
109
|
+
@spots << [node.location.start_offset, node.location.end_offset]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.rewrite (source)
|
|
114
|
+
visitor = Leaves.new
|
|
115
|
+
Prism.parse(source).value.accept(visitor)
|
|
116
|
+
out = source.dup
|
|
117
|
+
visitor.spots.sort_by { |start, _| -start }.each do |start, stop|
|
|
118
|
+
out[start...stop] = "::CArray::FuseSource.shadow(#{source[start...stop]})"
|
|
119
|
+
end
|
|
120
|
+
out
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# Turning a lazy expression into a plan a compiler can read.
|
|
3
|
+
#
|
|
4
|
+
# A lazy view is already a typed, closed expression graph, and the kernels
|
|
5
|
+
# already carry the C that computes each operation (CArray.__kernel_body__).
|
|
6
|
+
# What is missing between them is the reading: which operation each node is,
|
|
7
|
+
# what its mask does, and where the leaves are. That is what a plan holds.
|
|
8
|
+
#
|
|
9
|
+
# Nothing here compiles anything. A plan is plain data, and CArray itself
|
|
10
|
+
# never needs one -- it can always walk the view. What a plan is for is a
|
|
11
|
+
# second evaluator: hand it to one, and the answer must be the same.
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
class CArray
|
|
15
|
+
|
|
16
|
+
module Fusion
|
|
17
|
+
|
|
18
|
+
# One value per node, in evaluation order; the last is the result.
|
|
19
|
+
#
|
|
20
|
+
# Op an operation, reading the nodes named in `args`
|
|
21
|
+
# Leaf an array, the `index`-th of plan.leaves
|
|
22
|
+
# Const a scalar written into the expression
|
|
23
|
+
Op = Struct.new(:kind, :name, :data_type, :args, :body, :mask, :trapping)
|
|
24
|
+
Leaf = Struct.new(:index, :data_type, :masked)
|
|
25
|
+
Const = Struct.new(:value, :data_type)
|
|
26
|
+
|
|
27
|
+
Plan = Struct.new(:nodes, :leaves, :data_type, :dim, :masked, :signature)
|
|
28
|
+
|
|
29
|
+
LAZY_CLASSES = [CAMonOp, CABinOp, CATriOp, CAMonCmp, CABinCmp, CALazyMarker]
|
|
30
|
+
|
|
31
|
+
# A lazy node names its operation by an id and the kernels name it by a
|
|
32
|
+
# symbol. These are the same operations, spelled the way each side
|
|
33
|
+
# spells them; the rest are spelled alike.
|
|
34
|
+
BINOP_NAMES = {
|
|
35
|
+
:+ => :add, :- => :sub, :* => :mul,
|
|
36
|
+
:/ => :div, :** => :power, :% => :mod,
|
|
37
|
+
:& => :bit_and_i, :| => :bit_or_i, :^ => :bit_xor_i,
|
|
38
|
+
:<< => :bit_lshift, :>> => :bit_rshift,
|
|
39
|
+
}.freeze
|
|
40
|
+
TRIOP_NAMES = { :__clip_ki__ => :clip }.freeze
|
|
41
|
+
|
|
42
|
+
MONOP_BY_ID = CArray::LAZY_MONOP_OP_IDS.invert.freeze
|
|
43
|
+
BINOP_BY_ID = CArray::LAZY_BINOP_OP_IDS.invert.freeze
|
|
44
|
+
TRIOP_BY_ID = CArray::LAZY_TRIOP_OP_IDS.invert.freeze
|
|
45
|
+
|
|
46
|
+
# Integer division and its relatives raise on a zero divisor, so a cell
|
|
47
|
+
# the mask excludes must not be computed at all -- the divisor there is
|
|
48
|
+
# nobody's business (ca_binop_dispatch.c).
|
|
49
|
+
TRAPPING = %i[div mod quo_i fmod].freeze
|
|
50
|
+
INTEGERS = %i[int8 int16 int32 int64 uint8 uint16 uint32 uint64].freeze
|
|
51
|
+
|
|
52
|
+
class Refused < StandardError; end
|
|
53
|
+
|
|
54
|
+
# ---- who computes a plan --------------------------------------------
|
|
55
|
+
#
|
|
56
|
+
# CArray can always walk the expression, so nothing has to be registered
|
|
57
|
+
# and nothing changes when nothing is. What a registered evaluator adds
|
|
58
|
+
# is a second way to arrive at the same answer; it is asked, and it may
|
|
59
|
+
# decline. The dispatch point stays on CArray's side, which is what
|
|
60
|
+
# keeps the threshold below a decision about CArray's own walk rather
|
|
61
|
+
# than one that moves with whatever is installed.
|
|
62
|
+
#
|
|
63
|
+
# The evaluator itself is held by CArray (see carray/lazy.rb), so that
|
|
64
|
+
# materialising an expression need not reach for this file at all until
|
|
65
|
+
# something has been registered.
|
|
66
|
+
|
|
67
|
+
# Reaching a compiled kernel costs about the same whatever the array's
|
|
68
|
+
# size, and what it buys is the passes the walk would make. Below this
|
|
69
|
+
# the walk is the faster answer. The crossing moves with how wide the
|
|
70
|
+
# expression is -- measured, thirty thousand cells at one operation, six
|
|
71
|
+
# thousand at six -- and this brackets those: a one-operation expression
|
|
72
|
+
# loses a couple of microseconds here, a six-operation one wins ten.
|
|
73
|
+
THRESHOLD = 10_000
|
|
74
|
+
|
|
75
|
+
# Returns the array, or nil where nothing computed it.
|
|
76
|
+
def self.evaluate (view)
|
|
77
|
+
return nil unless askable?(view)
|
|
78
|
+
out = CArray.__alloc_uninit__(view.data_type, view.dim)
|
|
79
|
+
evaluate_into(view, out) ? out : nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Fills an array the caller already has. Called from the store as well,
|
|
83
|
+
# where making one and copying it over would be most of the work.
|
|
84
|
+
# Returns true when something computed it.
|
|
85
|
+
def self.evaluate_into (view, out)
|
|
86
|
+
evaluator = CArray.expression_evaluator or return false
|
|
87
|
+
return false unless askable?(view)
|
|
88
|
+
plan = plan(view) or return false
|
|
89
|
+
# A marker over an array, or anything else with nothing to compute,
|
|
90
|
+
# is not worth handing over.
|
|
91
|
+
return false unless plan.nodes.any? { |n| n.is_a?(Op) }
|
|
92
|
+
out.mask = 0 if plan.masked && ! out.has_mask?
|
|
93
|
+
evaluator.call(plan, out) ? true : false
|
|
94
|
+
rescue StandardError => error
|
|
95
|
+
CArray.expression_evaluator = nil
|
|
96
|
+
warn "CArray: the registered expression evaluator raised " \
|
|
97
|
+
"(#{error.class}: #{error.message}); expressions will be walked " \
|
|
98
|
+
"from here on"
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.askable? (view)
|
|
103
|
+
! CArray.expression_evaluator.nil? && view.elements >= THRESHOLD
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns a Plan, or nil where the expression holds something a plan
|
|
107
|
+
# cannot describe. Refusing is ordinary: the caller walks instead.
|
|
108
|
+
def self.plan (view)
|
|
109
|
+
build(view)
|
|
110
|
+
rescue Refused
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.build (view)
|
|
115
|
+
raise Refused, "not a lazy expression" unless lazy?(view)
|
|
116
|
+
w = Walk.new
|
|
117
|
+
w.visit(view)
|
|
118
|
+
Plan.new(w.nodes, w.leaves, view.data_type, view.dim,
|
|
119
|
+
w.leaves.any? { |a| a.has_mask? }, w.signature)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.lazy? (x)
|
|
123
|
+
LAZY_CLASSES.any? { |k| x.is_a?(k) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# ---- the walk -------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
class Walk
|
|
129
|
+
attr_reader :nodes, :leaves, :signature
|
|
130
|
+
|
|
131
|
+
def initialize
|
|
132
|
+
@nodes = []
|
|
133
|
+
@leaves = []
|
|
134
|
+
@seen = {}
|
|
135
|
+
@signature = +""
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def visit (n)
|
|
139
|
+
@seen[n.object_id] ||= build(n)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def build (n)
|
|
145
|
+
case n
|
|
146
|
+
when CALazyMarker then visit(n.parent)
|
|
147
|
+
when CAMonOp then unary(n)
|
|
148
|
+
when CABinOp then binary(n)
|
|
149
|
+
when CATriOp then ternary(n)
|
|
150
|
+
when CScalar then constant(n)
|
|
151
|
+
when CArray then leaf(n)
|
|
152
|
+
else raise Refused, "#{n.class} in an expression"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def unary (n)
|
|
157
|
+
name = spell(MONOP_BY_ID, n.__op_id__, {})
|
|
158
|
+
args = [visit(n.parent)]
|
|
159
|
+
# A view over one array is masked exactly where that array is
|
|
160
|
+
# (ca_obj_monop.c).
|
|
161
|
+
op(:monop, name, n.data_type, args, :pass)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def binary (n)
|
|
165
|
+
name = spell(BINOP_BY_ID, n.__op_id__, BINOP_NAMES)
|
|
166
|
+
args = [visit(n.parent), visit(n.__binop_right__)]
|
|
167
|
+
# Boolean `&` and `|` are three-valued: a masked cell whose known
|
|
168
|
+
# side settles the answer comes back unmasked (ca_obj_binop.c).
|
|
169
|
+
rule = if n.data_type == :boolean && name == :bit_or_i then :kleene_or
|
|
170
|
+
elsif n.data_type == :boolean && name == :bit_and_i then :kleene_and
|
|
171
|
+
else :union
|
|
172
|
+
end
|
|
173
|
+
op(:binop, name, n.data_type, args, rule)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def ternary (n)
|
|
177
|
+
name = spell(TRIOP_BY_ID, n.__op_id__, TRIOP_NAMES)
|
|
178
|
+
args = [visit(n.parent), visit(n.__triop_op2__), visit(n.__triop_op3__)]
|
|
179
|
+
op(:triop, name, n.data_type, args, :union)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def op (kind, name, type, args, mask)
|
|
183
|
+
body = CArray.__kernel_body__(kind, name, type) or
|
|
184
|
+
raise Refused, "#{kind} #{name} has no body at #{type}"
|
|
185
|
+
note(kind.to_s[0], name, type)
|
|
186
|
+
push Op.new(kind, name, type, args, body, mask,
|
|
187
|
+
TRAPPING.include?(name) && INTEGERS.include?(type))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def leaf (n)
|
|
191
|
+
note("a", n.data_type, n.has_mask? ? 1 : 0)
|
|
192
|
+
@leaves << n
|
|
193
|
+
push Leaf.new(@leaves.size - 1, n.data_type, n.has_mask?)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def constant (n)
|
|
197
|
+
note("k", n[0], n.data_type)
|
|
198
|
+
push Const.new(n[0], n.data_type)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def push (node)
|
|
202
|
+
@nodes << node
|
|
203
|
+
@nodes.size - 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def spell (table, id, renames)
|
|
207
|
+
ruby = table[id] or raise Refused, "operation id #{id}"
|
|
208
|
+
renames.fetch(ruby, ruby)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Two expressions of the same shape compute alike, whatever arrays
|
|
212
|
+
# they are over, so a consumer can keep one compiled kernel for both.
|
|
213
|
+
def note (*parts)
|
|
214
|
+
@signature << parts.join(":") << ";"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
data/lib/carray/histogram.rb
CHANGED
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
# Each sample contributes `w[i]` instead of 1 to its target cell.
|
|
95
95
|
#
|
|
96
96
|
# * **weights.shape** = chunk.shape minus the channel axis (= fiber + (A,)).
|
|
97
|
-
# * **
|
|
98
|
-
# fixes weighted vs unweighted; subsequent adds must match). Counts
|
|
97
|
+
# * **The data type** is locked at construction (= the entry method's `weights:` kwarg
|
|
98
|
+
# fixes weighted vs unweighted; subsequent adds must match). Counts type:
|
|
99
99
|
# int64 unweighted, float64 weighted. Weighted counts are always float64:
|
|
100
100
|
# the fused scatter kernel requires float64 weights, so integer weights are
|
|
101
101
|
# taken as float64 (= integer weighted counts are not supported).
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
# ### Composition
|
|
119
119
|
#
|
|
120
120
|
# `h1 + h2` returns a new Histogram with cells summed elementwise. Both
|
|
121
|
-
# operands must agree on edges / fiber_shape / include_max / weighted
|
|
121
|
+
# operands must agree on edges / fiber_shape / include_max / weighted data type
|
|
122
122
|
# (= the structure-level semantic guard); cells themselves are just
|
|
123
123
|
# integer / float tallies. See the `+` method.
|
|
124
124
|
#
|
|
@@ -135,7 +135,7 @@ class CArray
|
|
|
135
135
|
# {BincountND} instead.
|
|
136
136
|
class Histogram
|
|
137
137
|
|
|
138
|
-
# @overload initialize(edges:, fiber_shape: [], include_max: false,
|
|
138
|
+
# @overload initialize(edges:, fiber_shape: [], include_max: false, weights_data_type: nil)
|
|
139
139
|
# Allocates a new histogram accumulator.
|
|
140
140
|
# @param edges [Array<CArray, Array<Numeric>>] one edges array
|
|
141
141
|
# per histogram dimension; each must be 1-D sorted ascending
|
|
@@ -145,11 +145,11 @@ class CArray
|
|
|
145
145
|
# @param include_max [Boolean, Array<Boolean>] whether values
|
|
146
146
|
# equal to the last edge fold into the last bin; a scalar
|
|
147
147
|
# broadcasts across dimensions.
|
|
148
|
-
# @param
|
|
148
|
+
# @param weights_data_type [Symbol, nil] `data_type` of the
|
|
149
149
|
# accumulator when weighted; `nil` for a count-only
|
|
150
150
|
# accumulator (int64 counts).
|
|
151
151
|
# @return [Histogram]
|
|
152
|
-
def initialize (edges:, fiber_shape: [], include_max: false,
|
|
152
|
+
def initialize (edges:, fiber_shape: [], include_max: false, weights_data_type: nil)
|
|
153
153
|
@edges_list = edges.map { |e| CArray.wrap_readonly(e, :float64) }
|
|
154
154
|
raise ArgumentError, "edges must be a non-empty list" if @edges_list.empty?
|
|
155
155
|
@edges_list.each_with_index do |e, k|
|
|
@@ -166,11 +166,11 @@ class CArray
|
|
|
166
166
|
else
|
|
167
167
|
[!!include_max] * @m
|
|
168
168
|
end
|
|
169
|
-
@weighted = !
|
|
170
|
-
@
|
|
169
|
+
@weighted = !weights_data_type.nil?
|
|
170
|
+
@counts_data_type = @weighted ? weights_data_type : :int64
|
|
171
171
|
ext_dims = @n_list.map { |n| n + 2 }
|
|
172
172
|
ext_shape = @fiber_shape + ext_dims
|
|
173
|
-
@full_counts = CArray.public_send(@
|
|
173
|
+
@full_counts = CArray.public_send(@counts_data_type, *ext_shape).fill(0)
|
|
174
174
|
@sample_axis = nil
|
|
175
175
|
@channel_axis = nil
|
|
176
176
|
end
|
|
@@ -321,7 +321,7 @@ class CArray
|
|
|
321
321
|
|
|
322
322
|
if weights
|
|
323
323
|
raise ArgumentError, "weights given but accumulator is unweighted" unless @weighted
|
|
324
|
-
weights = CArray.wrap_readonly(weights, @
|
|
324
|
+
weights = CArray.wrap_readonly(weights, @counts_data_type)
|
|
325
325
|
expected_w_shape = chunk.shape.dup
|
|
326
326
|
expected_w_shape.delete_at(channel_ax)
|
|
327
327
|
unless weights.shape == expected_w_shape
|
|
@@ -379,7 +379,7 @@ class CArray
|
|
|
379
379
|
edges: @edges_list,
|
|
380
380
|
fiber_shape: @fiber_shape,
|
|
381
381
|
include_max: @include_max,
|
|
382
|
-
|
|
382
|
+
weights_data_type: @weighted ? @counts_data_type : nil)
|
|
383
383
|
rf = result.instance_variable_get(:@full_counts)
|
|
384
384
|
rf[] = @full_counts + other.full_counts
|
|
385
385
|
result.instance_variable_set(:@sample_axis, @sample_axis)
|
|
@@ -401,7 +401,7 @@ class CArray
|
|
|
401
401
|
# arr.shape = fiber_shape + (last M bin axes).
|
|
402
402
|
# Reduce along the last M axes, returns shape fiber_shape (or scalar).
|
|
403
403
|
#
|
|
404
|
-
# `accumulate` preserves
|
|
404
|
+
# `accumulate` preserves the data type (= int64 stays int64, float64 stays float64),
|
|
405
405
|
# unlike `sum` which always lifts to float64. Caveat: int64 overflows at
|
|
406
406
|
# ~9.2e18 (silent wrap); weighted float64 loses precision past 2^53 but
|
|
407
407
|
# does not overflow. Realistic histograms do not hit these limits.
|
|
@@ -497,15 +497,15 @@ class CArray
|
|
|
497
497
|
[sample_ax, channel_ax].sort.reverse.each { |p| fiber_shape.delete_at(p) }
|
|
498
498
|
|
|
499
499
|
# Weighted counts are float64-only (the fused scatter kernel requires
|
|
500
|
-
# float64 weights and float64 counts), so the
|
|
501
|
-
# than derived from the weights' own
|
|
502
|
-
|
|
500
|
+
# float64 weights and float64 counts), so the type is fixed here rather
|
|
501
|
+
# than derived from the weights' own type.
|
|
502
|
+
weights_data_type = (:float64 if weights)
|
|
503
503
|
|
|
504
504
|
h = Histogram.send(:new,
|
|
505
505
|
edges: edges,
|
|
506
506
|
fiber_shape: fiber_shape,
|
|
507
507
|
include_max: include_max,
|
|
508
|
-
|
|
508
|
+
weights_data_type: weights_data_type)
|
|
509
509
|
h.add(arr, axis: axis, weights: weights)
|
|
510
510
|
h
|
|
511
511
|
end
|
data/lib/carray/inspect.rb
CHANGED
data/lib/carray/iterator.rb
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
# those are NOT part of the common contract because some members legitimately
|
|
15
15
|
# omit them (a window has no map, a group has no within-piece min_index).
|
|
16
16
|
#
|
|
17
|
-
# The
|
|
18
|
-
# kernel_at_addr slot)
|
|
17
|
+
# The 2.0 generic dispatch (calculate / filter / evaluate over a
|
|
18
|
+
# kernel_at_addr slot) was retired in 3.0.
|
|
19
19
|
#
|
|
20
20
|
# ----------------------------------------------------------------------------
|
|
21
21
|
|
|
@@ -37,7 +37,8 @@ class CAIterator
|
|
|
37
37
|
# A member that genuinely cannot provide one overrides it to raise with its
|
|
38
38
|
# own reason.
|
|
39
39
|
[
|
|
40
|
-
:sum, :prod, :mean, :min, :max,
|
|
40
|
+
:sum, :accumulate, :prod, :mean, :min, :max, # tier 1
|
|
41
|
+
:variance, :stddev, :all, :any,
|
|
41
42
|
:variancep, :stddevp, :minmax, # tier 2
|
|
42
43
|
:min_index, :max_index, :min_addr, :max_addr, # position
|
|
43
44
|
:wsum, :wmean, # weighted
|