carray 3.0.6 → 3.0.7
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/CHANGELOG.md +110 -0
- data/README.md +24 -3
- data/carray.gemspec +1 -1
- data/ext/ca_axis_group.c +6 -6
- data/ext/ca_binop_dispatch.c +8 -0
- data/ext/ca_binop_dispatch.h +1 -0
- data/ext/ca_categorical_iterator.c +7 -7
- data/ext/ca_kernel_iterator.h +6 -4
- data/ext/ca_obj_array.c +4 -4
- data/ext/ca_obj_bincmp.c +19 -3
- data/ext/ca_obj_binop.c +36 -11
- data/ext/ca_obj_bitarray.c +2 -4
- data/ext/ca_obj_bitfield.c +86 -4
- data/ext/ca_obj_byte_swap.c +2 -4
- data/ext/ca_obj_const_string.c +422 -257
- data/ext/ca_obj_face.c +71 -33
- data/ext/ca_obj_fake.c +112 -13
- data/ext/ca_obj_farray.c +7 -1
- data/ext/ca_obj_fixlen_string.c +51 -17
- data/ext/ca_obj_grid.c +12 -7
- data/ext/ca_obj_meld.c +5 -5
- data/ext/ca_obj_monop.c +65 -39
- data/ext/ca_obj_object.c +9 -9
- data/ext/ca_obj_reduce.c +2 -4
- data/ext/ca_obj_refer.c +104 -10
- data/ext/ca_obj_repeat.c +1 -1
- data/ext/ca_obj_roll.c +2 -4
- data/ext/ca_obj_select.c +8 -6
- data/ext/ca_obj_select_axis.c +8 -6
- data/ext/ca_obj_stack.c +5 -5
- data/ext/ca_obj_stride.c +46 -8
- data/ext/ca_obj_string.c +2 -2
- data/ext/ca_obj_tile.c +2 -4
- data/ext/ca_obj_triop.c +15 -12
- data/ext/ca_obj_window.c +22 -15
- data/ext/ca_op_ipower.c +63 -15
- data/ext/ca_op_powi.h +6 -29
- data/ext/ca_sort_kernels.h +3 -0
- data/ext/ca_sweep_engine.c +90 -23
- data/ext/ca_sweep_engine.h +5 -6
- data/ext/caframe_csv_reader.c +1 -1
- data/ext/carray.h +3 -1
- data/ext/carray_access.c +98 -39
- data/ext/carray_address_basis.c +98 -24
- data/ext/carray_broadcast.c +2 -2
- data/ext/carray_call_cfunc.c +5 -8
- data/ext/carray_cast.c +311 -37
- data/ext/carray_cast_func.rb +37 -5
- data/ext/carray_conversion.c +56 -2
- data/ext/carray_copy.c +24 -0
- data/ext/carray_core.c +69 -6
- data/ext/carray_count.c +28 -9
- data/ext/carray_element.c +9 -3
- data/ext/carray_factorize.c +11 -11
- data/ext/carray_histogram.c +2 -2
- data/ext/carray_index_classifier.c +7 -5
- data/ext/carray_internal.h +94 -0
- data/ext/carray_kernels_binop.c +55 -55
- data/ext/carray_kernels_init.c +14 -14
- data/ext/carray_kernels_monop.c +90 -140
- data/ext/carray_lazy.c +16 -1
- data/ext/carray_loop.c +9 -2
- data/ext/carray_mask.c +118 -11
- data/ext/carray_median_percentile.c +13 -8
- data/ext/carray_memory_view.c +41 -22
- data/ext/carray_operator.c +72 -11
- data/ext/carray_order.c +52 -9
- data/ext/carray_partition.c +1 -1
- data/ext/carray_random.c +1 -1
- data/ext/carray_round.c +24 -1
- data/ext/carray_scatter.c +4 -0
- data/ext/carray_segment.c +1 -1
- data/ext/carray_slab.c +13 -4
- data/ext/carray_sort.c +6 -3
- data/ext/carray_sort_kernel.c +24 -0
- data/ext/carray_struct.c +21 -6
- data/ext/carray_test.c +85 -39
- data/ext/carray_time_text.c +6 -0
- data/ext/carray_utils.c +124 -54
- data/ext/mk_call_cfunc.rb +6 -11
- data/ext/mkkernel.rb +37 -27
- data/ext/version.h +4 -4
- data/lib/carray/axis_group.rb +9 -6
- data/lib/carray/basics.rb +26 -8
- data/lib/carray/bincount_nd.rb +8 -2
- data/lib/carray/block_iterator.rb +48 -21
- data/lib/carray/categorical.rb +21 -8
- data/lib/carray/categorical_iterator.rb +2 -1
- data/lib/carray/conditional.rb +52 -4
- data/lib/carray/const_string.rb +74 -0
- data/lib/carray/construct.rb +2 -2
- data/lib/carray/core_extensions.rb +19 -68
- data/lib/carray/data_type_extension.rb +1 -0
- data/lib/carray/fixlen_string.rb +83 -1
- data/lib/carray/frame/column_verbs.rb +61 -6
- data/lib/carray/frame/concat.rb +50 -6
- data/lib/carray/frame/csv_parser.rb +13 -4
- data/lib/carray/frame/frame.rb +38 -6
- data/lib/carray/frame/group.rb +67 -40
- data/lib/carray/frame/io.rb +10 -5
- data/lib/carray/frame/join.rb +20 -6
- data/lib/carray/frame/records.rb +6 -4
- data/lib/carray/frame/reshape.rb +25 -5
- data/lib/carray/frame/verbs.rb +78 -25
- data/lib/carray/frame/windows.rb +30 -6
- data/lib/carray/fuse_source.rb +4 -2
- data/lib/carray/iterator.rb +8 -0
- data/lib/carray/key_windows.rb +10 -4
- data/lib/carray/lazy.rb +12 -17
- data/lib/carray/methods/bin.rb +3 -0
- data/lib/carray/methods/composition.rb +1 -1
- data/lib/carray/methods/factorize.rb +2 -0
- data/lib/carray/methods/gather_nd.rb +4 -1
- data/lib/carray/methods/is_in.rb +23 -3
- data/lib/carray/methods/locate_addr.rb +63 -12
- data/lib/carray/methods/repeat.rb +11 -1
- data/lib/carray/record_order.rb +35 -6
- data/lib/carray/runtime.rb +10 -0
- data/lib/carray/segment_iterator.rb +19 -16
- data/lib/carray/serialize.rb +22 -0
- data/lib/carray/slab_iterator.rb +1 -1
- data/lib/carray/stack.rb +3 -3
- data/lib/carray/string.rb +6 -6
- data/lib/carray/string_operation_extension.rb +25 -4
- data/lib/carray/struct_builder.rb +4 -1
- data/lib/carray/time.rb +190 -46
- data/lib/carray/window_iterator.rb +48 -12
- data/yard-stubs/ca_obj_bitfield.rb +4 -1
- data/yard-stubs/ca_obj_refer.rb +3 -0
- data/yard-stubs/ca_obj_window.rb +2 -1
- data/yard-stubs/carray_access.rb +4 -2
- data/yard-stubs/carray_cast.rb +6 -0
- data/yard-stubs/carray_count.rb +6 -3
- data/yard-stubs/carray_math.rb +5 -0
- data/yard-stubs/carray_order.rb +6 -0
- data/yard-stubs/carray_scatter.rb +2 -1
- data/yard-stubs/carray_test.rb +18 -5
- metadata +2 -2
data/lib/carray/frame/verbs.rb
CHANGED
|
@@ -111,7 +111,7 @@ class CAFrame
|
|
|
111
111
|
#
|
|
112
112
|
# What the block returns becomes the column as it is: a view stays a view
|
|
113
113
|
# of what it was made from, so a write to a cell reaches it (+copy+ cuts
|
|
114
|
-
# that).
|
|
114
|
+
# that). A lazy expression is evaluated, as a column holds its values.
|
|
115
115
|
#
|
|
116
116
|
# @param names [Array<String, Range, Regexp>] the columns; default all.
|
|
117
117
|
# @yieldparam col [CArray] a column.
|
|
@@ -190,6 +190,9 @@ class CAFrame
|
|
|
190
190
|
pieces.each { |p| p[] = UNDEF }
|
|
191
191
|
time_text_of(col).to_a.each_with_index do |cell, i|
|
|
192
192
|
next if UNDEF.equal?(cell) || cell.nil?
|
|
193
|
+
unless cell.is_a?(String)
|
|
194
|
+
raise ArgumentError, "split_column: row #{i} of #{key.inspect} holds #{cell.inspect}, which is not text"
|
|
195
|
+
end
|
|
193
196
|
cell.split(sep, n).each_with_index { |part, k| pieces[k][i] = part }
|
|
194
197
|
end
|
|
195
198
|
rebuilt = {}
|
|
@@ -327,7 +330,15 @@ class CAFrame
|
|
|
327
330
|
raise ArgumentError,
|
|
328
331
|
"stack_columns: shape: #{layer.inspect} does not hold the #{names.size} columns"
|
|
329
332
|
end
|
|
330
|
-
|
|
333
|
+
CAFrame.__send__(:refuse_rounded_pieces, parts, "stack_columns", "column #{names.first.inspect}")
|
|
334
|
+
begin
|
|
335
|
+
col = CArray.stack(parts, axis: 1)
|
|
336
|
+
rescue ArgumentError => e
|
|
337
|
+
raise unless e.message.start_with?("stack: ")
|
|
338
|
+
raise ArgumentError,
|
|
339
|
+
"stack_columns: the columns do not meet as one column " \
|
|
340
|
+
"(#{e.message.delete_prefix("stack: ")}); cast them to one type first"
|
|
341
|
+
end
|
|
331
342
|
col = col.reshape(nrow, *layer, *trailing) if layer.size > 1
|
|
332
343
|
rebuilt = {}
|
|
333
344
|
@columns.each do |k, v|
|
|
@@ -548,7 +559,7 @@ class CAFrame
|
|
|
548
559
|
end
|
|
549
560
|
resolve_cast_map(name_or_map, label)
|
|
550
561
|
else
|
|
551
|
-
{ column_key(name_or_map, label) => type }
|
|
562
|
+
resolve_cast_map({ column_key(name_or_map, label) => type }, label)
|
|
552
563
|
end
|
|
553
564
|
# Read every column before rebinding any, so a raise leaves the frame as
|
|
554
565
|
# it was.
|
|
@@ -579,13 +590,15 @@ class CAFrame
|
|
|
579
590
|
named.each_key do |name|
|
|
580
591
|
refuse_missing_column(name, label.chomp(":")) unless @columns.key?(name)
|
|
581
592
|
end
|
|
582
|
-
|
|
593
|
+
inferred, named = named.partition { |_, t| t == :infer }.map(&:to_h)
|
|
594
|
+
rest = @columns.keys - named.keys - inferred.keys
|
|
583
595
|
types =
|
|
584
596
|
case default
|
|
585
597
|
when nil then {}
|
|
586
598
|
when :infer then rest.empty? ? {} : select(*rest).infer_types
|
|
587
599
|
else rest.to_h { |name| [name, default] }
|
|
588
600
|
end
|
|
601
|
+
types.merge!(select(*inferred.keys).infer_types) unless inferred.empty?
|
|
589
602
|
types.merge!(named.compact)
|
|
590
603
|
end
|
|
591
604
|
|
|
@@ -610,11 +623,13 @@ class CAFrame
|
|
|
610
623
|
# none present is not listed.
|
|
611
624
|
#
|
|
612
625
|
# A number with a leading zero ("007") is a code and keeps its column as
|
|
613
|
-
# text, and so does an integer too long for int64 (an identifier),
|
|
614
|
-
# float would round. A day-first or
|
|
615
|
-
# read as time, since which one it
|
|
616
|
-
# are looked at -- an object column
|
|
617
|
-
# CAConstString); columns that already have a
|
|
626
|
+
# text, and so does an integer too long for int64 (an identifier), or one
|
|
627
|
+
# past 2**53 among decimals, which a float would round. A day-first or
|
|
628
|
+
# month-first date ("01/02/2024") is not read as time, since which one it
|
|
629
|
+
# is cannot be told. Only text columns are looked at -- an object column
|
|
630
|
+
# or a string Face (CAString, CAConstString); columns that already have a
|
|
631
|
+
# type are not listed. An object column's cells are read as their text,
|
|
632
|
+
# so Ruby numbers there are typed as that text would be.
|
|
618
633
|
#
|
|
619
634
|
# df.cast(df.infer_types) # what cast(:infer) does
|
|
620
635
|
# df.cast(df.infer_types.merge("code" => :int32)) # as cast(default: :infer, "code" => :int32)
|
|
@@ -622,18 +637,30 @@ class CAFrame
|
|
|
622
637
|
@columns.each_with_object({}) do |(key, col), types|
|
|
623
638
|
next unless string_column?(col)
|
|
624
639
|
type = text_for_decimal_reading(col).__infer_number_type_of_text__
|
|
640
|
+
next if type == :float64 && integer_text_past_float?(col)
|
|
625
641
|
type ||= :boolean if boolean_word_column?(col)
|
|
626
642
|
type ||= :time if first_present_cell_can_be_time_text?(col) && time_text_column?(time_text_of(col))
|
|
627
643
|
types[key] = type if type
|
|
628
644
|
end
|
|
629
645
|
end
|
|
630
646
|
|
|
647
|
+
# Whether a cell is an integer a float64 would round. Fewer than 16 digits
|
|
648
|
+
# cannot be, which leaves a const string column's other cells unread.
|
|
649
|
+
private def integer_text_past_float?(col)
|
|
650
|
+
long = col.is_a?(CAConstString) ? col[col.byte_length.ge(16).strip_mask(false)].to_a : time_text_of(col).to_a
|
|
651
|
+
long.any? do |text|
|
|
652
|
+
next text.abs > 2**53 if text.is_a?(Integer) # an object cell read as its text
|
|
653
|
+
text.is_a?(String) && text.match?(/\A\s*[-+]?\d{16,}\s*\z/) && Integer(text.strip, 10).abs > 2**53
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
631
657
|
# Bring every column to one common data type and rebind them (memo §11.4).
|
|
632
658
|
# The frame-level counterpart of +CArray.promote_list+: where +cast+ forces
|
|
633
659
|
# named columns to a type, +promote+ widens the whole table until it has a
|
|
634
|
-
# single type. Returns a new frame;
|
|
635
|
-
#
|
|
636
|
-
#
|
|
660
|
+
# single type. Returns a new frame; a column that changes type is a fresh
|
|
661
|
+
# one, and one that has the type already is shared with this frame, as
|
|
662
|
+
# +promote+ with no argument leaves it. The index is not a column and is
|
|
663
|
+
# left alone.
|
|
637
664
|
#
|
|
638
665
|
# df.promote # the common type CArray.result_type would pick
|
|
639
666
|
# df.promote(:object) # force the widest type: anything fits
|
|
@@ -829,6 +856,9 @@ class CAFrame
|
|
|
829
856
|
# A constant fill, in place, naming the column when the value does not go
|
|
830
857
|
# into it, as strip_mask names it.
|
|
831
858
|
private def fill_constant (key, col, value)
|
|
859
|
+
if col.read_only?
|
|
860
|
+
raise "fill: column #{key.inspect} (#{CArray::Inspector.type_label(col)}) can not modify read-only array"
|
|
861
|
+
end
|
|
832
862
|
col.unmask(value)
|
|
833
863
|
rescue ArgumentError, TypeError, RangeError, FloatDomainError => e
|
|
834
864
|
raise e.exception("fill: column #{key.inspect} (#{CArray::Inspector.type_label(col)}) " \
|
|
@@ -962,6 +992,7 @@ class CAFrame
|
|
|
962
992
|
"fill: give a method or value after the column #{how.inspect}; " \
|
|
963
993
|
"a String alone is a column name"
|
|
964
994
|
end
|
|
995
|
+
refuse_unknown_fill_method(how, "fill")
|
|
965
996
|
map = @columns.select { |key, col| !key_names.include?(key) && col.any_masked? }
|
|
966
997
|
.keys.to_h { |key| [key, how] }
|
|
967
998
|
return map.empty? ? nil : map
|
|
@@ -970,16 +1001,18 @@ class CAFrame
|
|
|
970
1001
|
return names_selected(names, "fill").to_h { |key| [key, how] }
|
|
971
1002
|
end
|
|
972
1003
|
|
|
1004
|
+
private def refuse_unknown_fill_method (how, verb)
|
|
1005
|
+
return unless how.is_a?(Symbol) && !%i[ffill bfill linear].include?(how)
|
|
1006
|
+
raise ArgumentError,
|
|
1007
|
+
"#{verb}: unknown method #{how.inspect} (:ffill | :bfill | :linear, or a constant fill value)"
|
|
1008
|
+
end
|
|
1009
|
+
|
|
973
1010
|
private def fill_by_map (map, verb)
|
|
974
1011
|
raise ArgumentError, "#{verb}: the Hash names no column" if map.empty?
|
|
975
1012
|
refuse_if_frozen
|
|
976
1013
|
keys = column_keys(map.keys, verb)
|
|
977
1014
|
keys.each { |key| refuse_missing_column(key) unless @columns.key?(key) }
|
|
978
|
-
map.each_value
|
|
979
|
-
next unless how.is_a?(Symbol) && !%i[ffill bfill linear].include?(how)
|
|
980
|
-
raise ArgumentError,
|
|
981
|
-
"#{verb}: unknown method #{how.inspect} (:ffill | :bfill | :linear, or a constant fill value)"
|
|
982
|
-
end
|
|
1015
|
+
map.each_value { |how| refuse_unknown_fill_method(how, verb) }
|
|
983
1016
|
pairs = keys.zip(map.values)
|
|
984
1017
|
pairs.each { |key, how| yield self, key, how if @columns[key].read_only? }
|
|
985
1018
|
filled = pairs.map do |key, how|
|
|
@@ -988,10 +1021,18 @@ class CAFrame
|
|
|
988
1021
|
yield probe, key, how
|
|
989
1022
|
[key, probe[key]]
|
|
990
1023
|
end
|
|
991
|
-
filled.compact.each { |key, col| @columns[key]
|
|
1024
|
+
filled.compact.each { |key, col| write_filled_cells(@columns[key], col) }
|
|
992
1025
|
return self
|
|
993
1026
|
end
|
|
994
1027
|
|
|
1028
|
+
# Only the cells a fill gave a value: a view may show one stored cell
|
|
1029
|
+
# twice, and writing a copy that stayed masked would mask it again.
|
|
1030
|
+
private def write_filled_cells(col, filled)
|
|
1031
|
+
return unless col.has_mask?
|
|
1032
|
+
target = col.is_masked & filled.is_not_masked
|
|
1033
|
+
col[target] = filled[target] if target.any
|
|
1034
|
+
end
|
|
1035
|
+
|
|
995
1036
|
private def fill_linear_column(key, col, verb = "fill")
|
|
996
1037
|
# A Face column interpolates through its own linear_fetch (a time
|
|
997
1038
|
# column's keeps the Face and its unit), as the core gap fill does.
|
|
@@ -1021,7 +1062,7 @@ class CAFrame
|
|
|
1021
1062
|
return
|
|
1022
1063
|
end
|
|
1023
1064
|
# The same fill as the core, with the index as x.
|
|
1024
|
-
col
|
|
1065
|
+
write_filled_cells(col, col.send(:linear_fill_along, @index))
|
|
1025
1066
|
end
|
|
1026
1067
|
|
|
1027
1068
|
# Text cells are data, not Ruby literals: an object column cast to a
|
|
@@ -1038,7 +1079,7 @@ class CAFrame
|
|
|
1038
1079
|
if type == :string
|
|
1039
1080
|
return col if col.is_a?(CArray::StringOperationMixin)
|
|
1040
1081
|
begin
|
|
1041
|
-
return CArray.string(col)
|
|
1082
|
+
return CArray.string(col).copy
|
|
1042
1083
|
rescue CArray::DataTypeError, TypeError => e
|
|
1043
1084
|
raise e.class, "column #{key.inspect} cannot be cast to :string: #{e.message}"
|
|
1044
1085
|
end
|
|
@@ -1154,9 +1195,11 @@ class CAFrame
|
|
|
1154
1195
|
next unless present[addr]
|
|
1155
1196
|
text = cells[addr]
|
|
1156
1197
|
m = text.is_a?(String) && TIMEDELTA_TEXT.match(text)
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1198
|
+
value = m && Integer(m[1], 10)
|
|
1199
|
+
count = m && (m[2] ? Integer(m[2], 10) : 1)
|
|
1200
|
+
if m && value.bit_length < 64 && count.positive? && count.bit_length < 64
|
|
1201
|
+
values[addr] = value
|
|
1202
|
+
units[addr] = CATime::Resolution.new(count, m[3].to_sym)
|
|
1160
1203
|
elsif !text.nil? && !(text.is_a?(String) && text.strip.empty?)
|
|
1161
1204
|
bad << addr
|
|
1162
1205
|
end
|
|
@@ -1175,7 +1218,11 @@ class CAFrame
|
|
|
1175
1218
|
ticks[nil] = UNDEF
|
|
1176
1219
|
distinct.each do |u|
|
|
1177
1220
|
addrs = units.each_index.select { |i| units[i] == u }
|
|
1178
|
-
|
|
1221
|
+
begin
|
|
1222
|
+
scaled = CATimeUnitAlgebra.convert_scale!(CA_INT64(addrs.map { |i| values[i] }), u, unit)
|
|
1223
|
+
rescue RangeError
|
|
1224
|
+
raise RangeError, "cast: column #{key.inspect} holds durations too long for their finest unit, #{unit}"
|
|
1225
|
+
end
|
|
1179
1226
|
ticks[CA_INT64(addrs)] = scaled
|
|
1180
1227
|
end
|
|
1181
1228
|
ticks.reshape(*col.shape).timedelta(unit: unit)
|
|
@@ -1421,7 +1468,13 @@ class CAFrame
|
|
|
1421
1468
|
# to say about a surface it cannot read, so the widening check -- which
|
|
1422
1469
|
# is about primitive promotion -- applies to plain columns only.
|
|
1423
1470
|
@columns.each { |key, col| refuse_narrowing(key, col, type) unless col.face? }
|
|
1424
|
-
|
|
1471
|
+
# A column that has the type already stays as it is, as promote leaves it
|
|
1472
|
+
# with no target.
|
|
1473
|
+
name = CArray.data_type_name(type)
|
|
1474
|
+
@columns.each_key do |key|
|
|
1475
|
+
col = @columns[key]
|
|
1476
|
+
@columns[key] = col.to_type(type) unless !col.face? && col.data_type_name == name
|
|
1477
|
+
end
|
|
1425
1478
|
end
|
|
1426
1479
|
|
|
1427
1480
|
private def refuse_narrowing(key, col, type)
|
data/lib/carray/frame/windows.rb
CHANGED
|
@@ -61,7 +61,7 @@ class WindowedFrame
|
|
|
61
61
|
# Returns the CAKeyWindowIterator of one column.
|
|
62
62
|
# @return [CAKeyWindowIterator]
|
|
63
63
|
def [] (name)
|
|
64
|
-
@plan.for_value(@frame
|
|
64
|
+
@plan.for_value(@frame.__send__(:named_column, @frame.__send__(:column_key, name, "windows"), "windows"))
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# What each window covered, one value per row: see CAKeyWindowIterator.
|
|
@@ -113,13 +113,14 @@ class WindowedFrame
|
|
|
113
113
|
# @param names [Array<String>] the columns; default every column that has
|
|
114
114
|
# a quantile.
|
|
115
115
|
# @return [CAFrame]
|
|
116
|
-
def quantile (*names)
|
|
116
|
+
def quantile (*names, **keywords)
|
|
117
|
+
refuse_keywords(keywords)
|
|
117
118
|
named = {}
|
|
118
119
|
over_named_columns(:quantile, names, @key_names) do |col, name|
|
|
119
120
|
named[name] = @plan.for_value(col).quantile
|
|
120
121
|
end
|
|
121
122
|
cols = CAFrame.__send__(:quantile_columns, named)
|
|
122
|
-
return CAFrame.new(cols, axis_name: @frame.axis_name, index: @frame.index) unless @at
|
|
123
|
+
return CAFrame.new(cols, axis_name: @frame.axis_name, index: @frame.index&.copy) unless @at
|
|
123
124
|
key = @frame.__send__(:key_column, @key_names.first, "windows")
|
|
124
125
|
CAFrame.new(cols, axis_name: @key_names.first, index: CAFrame.__send__(:key_labels, key, @at.copy))
|
|
125
126
|
end
|
|
@@ -130,17 +131,40 @@ class WindowedFrame
|
|
|
130
131
|
window_reduce(reduction, names, min_count: min_count, fill_value: fill_value)
|
|
131
132
|
end
|
|
132
133
|
|
|
133
|
-
def window_value (reduction, *names)
|
|
134
|
+
def window_value (reduction, *names, **keywords)
|
|
135
|
+
refuse_keywords(keywords)
|
|
134
136
|
window_reduce(reduction, names)
|
|
135
137
|
end
|
|
136
138
|
|
|
139
|
+
# These reductions take no keyword. Without the **keywords above, Ruby
|
|
140
|
+
# would hand a keyword in as a column name.
|
|
141
|
+
def refuse_keywords (keywords)
|
|
142
|
+
return if keywords.empty?
|
|
143
|
+
list = keywords.keys.map(&:inspect).join(", ")
|
|
144
|
+
raise ArgumentError, "unknown keyword#{keywords.size > 1 ? "s" : ""}: #{list}"
|
|
145
|
+
end
|
|
146
|
+
|
|
137
147
|
# An N-D column (each row a profile) is reduced along the rows, each
|
|
138
148
|
# window keeping the trailing shape, as a group reduces it.
|
|
139
149
|
def window_reduce (reduction, names, args = [], **keywords)
|
|
140
150
|
keywords = keywords.compact
|
|
151
|
+
if @at && names.include?(@key_names.first)
|
|
152
|
+
raise ArgumentError, "#{reduction}: #{@key_names.first.inspect} is the key, already the result's index"
|
|
153
|
+
end
|
|
154
|
+
fills = keywords.delete(:fill_value)
|
|
155
|
+
refuse_unknown_fill_columns(reduction, fills)
|
|
156
|
+
refuse_wrong_reduction_arguments(reduction, args, keywords)
|
|
141
157
|
named = {}
|
|
142
158
|
out = over_named_columns(reduction, names, @key_names, args) do |col, name|
|
|
143
|
-
|
|
159
|
+
fill = fills.is_a?(Hash) ? fills[name] : fills
|
|
160
|
+
kw = fill.nil? ? keywords : keywords.merge(fill_value: fill)
|
|
161
|
+
named[name] =
|
|
162
|
+
if %i[first last].include?(reduction) && col.ndim == 1 && args.empty? && kw.empty? &&
|
|
163
|
+
col.face? && !col.__send__(:holds_new_values?)
|
|
164
|
+
col[@plan.for_value(present_rows(col)).public_send(reduction)].copy
|
|
165
|
+
else
|
|
166
|
+
@plan.for_value(col).public_send(reduction, *args, **kw)
|
|
167
|
+
end
|
|
144
168
|
end
|
|
145
169
|
out = named if @at && !names.empty?
|
|
146
170
|
return out if out.is_a?(CArray) && !@at
|
|
@@ -149,7 +173,7 @@ class WindowedFrame
|
|
|
149
173
|
key = @frame.__send__(:key_column, @key_names.first, "windows")
|
|
150
174
|
CAFrame.new(out, axis_name: @key_names.first, index: CAFrame.__send__(:key_labels, key, @at.copy))
|
|
151
175
|
else
|
|
152
|
-
CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
|
|
176
|
+
CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index&.copy)
|
|
153
177
|
end
|
|
154
178
|
end
|
|
155
179
|
end
|
data/lib/carray/fuse_source.rb
CHANGED
|
@@ -185,7 +185,9 @@ class CArray
|
|
|
185
185
|
# block that starts on the sequence's first line is found in the file's
|
|
186
186
|
# syntax tree. Only when two start on that line is the sequence itself
|
|
187
187
|
# dumped: InstructionSequence#to_a reads the instructions while another
|
|
188
|
-
# Ractor may be running them, and that can crash the process.
|
|
188
|
+
# Ractor may be running them, and that can crash the process. Two Ractors
|
|
189
|
+
# dumping one sequence for the first time crash Ruby before 4.1, so the
|
|
190
|
+
# dump goes through a lock.
|
|
189
191
|
def self.code_location (sequence, path)
|
|
190
192
|
line = sequence.first_lineno
|
|
191
193
|
found = []
|
|
@@ -206,7 +208,7 @@ class CArray
|
|
|
206
208
|
start, whole = found.first
|
|
207
209
|
[start.start_line, start.start_column, whole.end_line, whole.end_column]
|
|
208
210
|
else
|
|
209
|
-
sequence
|
|
211
|
+
CArray.send(:instruction_sequence_to_a, sequence)[4][:code_location]
|
|
210
212
|
end
|
|
211
213
|
end
|
|
212
214
|
|
data/lib/carray/iterator.rb
CHANGED
|
@@ -256,6 +256,14 @@ class CAIterator
|
|
|
256
256
|
end))
|
|
257
257
|
end
|
|
258
258
|
|
|
259
|
+
# CArray#count over a view: CABlock / CAWindow shadow #count with a geometry
|
|
260
|
+
# accessor, so it is dispatched explicitly, while a Face keeps a #count of
|
|
261
|
+
# its own (CAConstString reads its cells as Strings).
|
|
262
|
+
private def count_cells (view, *args, **kw)
|
|
263
|
+
return view.count(*args, **kw) if view.face?
|
|
264
|
+
CArray.instance_method(:count).bind_call(view, *args, **kw)
|
|
265
|
+
end
|
|
266
|
+
|
|
259
267
|
# Recommended (should) surface -- template methods. These are well-defined
|
|
260
268
|
# for some members and structurally impossible for others, so they are not
|
|
261
269
|
# required: `map` is a per-piece element-wise transform scattered back to the
|
data/lib/carray/key_windows.rb
CHANGED
|
@@ -76,7 +76,9 @@ class CAKeyWindowIterator < CAIterator
|
|
|
76
76
|
raise ArgumentError, "windows: at: is a one-dimensional CArray of points"
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
|
-
|
|
79
|
+
# Taken as it is now, as the windows are: first_key, last_key and bounds
|
|
80
|
+
# read it later.
|
|
81
|
+
@key = on.copy
|
|
80
82
|
@n = value.dim0
|
|
81
83
|
# Taken as they are now, as the windows are: bounds reads them later.
|
|
82
84
|
@points = at&.copy
|
|
@@ -412,9 +414,13 @@ class CAKeyWindowIterator < CAIterator
|
|
|
412
414
|
if sample.is_a?(Array)
|
|
413
415
|
return sample.each_index.map { |k| lay_out(results.map { |r| r[k] }, sample[k], trail) }
|
|
414
416
|
end
|
|
415
|
-
|
|
417
|
+
# A Face that holds no new value cannot be written cell by cell, so its
|
|
418
|
+
# values are held as the surface objects they are.
|
|
419
|
+
surface = sample.face? && !sample.__send__(:holds_new_values?)
|
|
420
|
+
shape = [@nout, *trail]
|
|
421
|
+
out = surface ? CArray.new(CA_OBJECT, shape) : sample.__send__(:blank_like, shape)
|
|
416
422
|
flat = out.reshape(@nout, results.size)
|
|
417
|
-
results.each_with_index { |r, j| flat[nil, j] = r }
|
|
423
|
+
results.each_with_index { |r, j| flat[nil, j] = surface ? r.to_a : r }
|
|
418
424
|
out
|
|
419
425
|
end
|
|
420
426
|
|
|
@@ -548,7 +554,7 @@ class CAKeyWindowIterator < CAIterator
|
|
|
548
554
|
|
|
549
555
|
def window_bound (width, op)
|
|
550
556
|
key = @points || @key
|
|
551
|
-
return key if width.nil?
|
|
557
|
+
return key.copy if width.nil?
|
|
552
558
|
if key.is_a?(CATime) || key.is_a?(CATimedelta)
|
|
553
559
|
key = key.to_unit(CATimeUnitAlgebra.finer(key.unit, width.unit))
|
|
554
560
|
end
|
data/lib/carray/lazy.rb
CHANGED
|
@@ -430,26 +430,20 @@ class CArray
|
|
|
430
430
|
private :clamp_between
|
|
431
431
|
LazyDispatch.send(:private, :clamp_between)
|
|
432
432
|
|
|
433
|
-
# `**`
|
|
433
|
+
# `**` with an Integer exponent.
|
|
434
434
|
#
|
|
435
|
-
# The
|
|
436
|
-
#
|
|
437
|
-
#
|
|
438
|
-
# `
|
|
439
|
-
#
|
|
440
|
-
# form). Mirror that dispatch here so `a.lazy ** 2` rides the same
|
|
441
|
-
# fast path.
|
|
442
|
-
#
|
|
443
|
-
# Routing rule (matches eager `rb_ca_pow`):
|
|
435
|
+
# The eager `rb_ca_pow` sends a Float / Complex array to an Integer power
|
|
436
|
+
# through `rb_ca_ipower` (ext/ca_op_ipower.c), which keeps the exponent
|
|
437
|
+
# an int64 and computes each cell with pow / powf / cpow, the answer the
|
|
438
|
+
# `power` kernel gives for the same exponent held in an array. The lazy
|
|
439
|
+
# form builds the matching CABinOp(OP_IPOWER) node:
|
|
444
440
|
# - lazy parent + Integer exponent + Float/Complex parent data_type
|
|
445
441
|
# -> wrap exponent as CScalar.int64 + CABinOp(OP_IPOWER)
|
|
446
|
-
# - lazy parent (other cases) -> CABinOp(OP_POW)
|
|
447
|
-
# - else -> eager
|
|
442
|
+
# - lazy parent (other cases) -> CABinOp(OP_POW)
|
|
443
|
+
# - else -> eager
|
|
448
444
|
#
|
|
449
|
-
# Integer parents
|
|
450
|
-
# `
|
|
451
|
-
# via the `int:` expr in MkKernel.binop :power, so OP_POW + int parent
|
|
452
|
-
# is already fast. The lazy-substrate gap was Float/Complex only.
|
|
445
|
+
# Integer parents stay on OP_POW: the `power` kernel's `int:` expr is
|
|
446
|
+
# `op_powi_<type>`, binary exponentiation in the integer type.
|
|
453
447
|
module LazyDispatch
|
|
454
448
|
def **(other)
|
|
455
449
|
if ( lazy_view? || (other.is_a?(CArray) && other.lazy_view?) ) &&
|
|
@@ -498,7 +492,8 @@ class CArray
|
|
|
498
492
|
# `eq` / `feq`.
|
|
499
493
|
#
|
|
500
494
|
# A comparison with UNDEF goes to the eager method, which answers `eq` /
|
|
501
|
-
# `ne` from the mask and refuses the others
|
|
495
|
+
# `ne` from the mask and refuses the others, and so does one with nil on
|
|
496
|
+
# an array that can not hold it.
|
|
502
497
|
#
|
|
503
498
|
# `feq` is arity 1 in eager (compile-time FLT_EPSILON / DBL_EPSILON);
|
|
504
499
|
# we mirror that here. Runtime eps is a future extension (struct field
|
data/lib/carray/methods/bin.rb
CHANGED
|
@@ -105,6 +105,9 @@ class CArray
|
|
|
105
105
|
# @raise [ArgumentError] when `edges` is not 1-D, has fewer than 2
|
|
106
106
|
# values, has a masked value or NaN, or decreases anywhere. Equal
|
|
107
107
|
# edges (a bin of zero width) and infinite ones are accepted.
|
|
108
|
+
# Values and edges are compared as float64, so int64 values beyond
|
|
109
|
+
# 2**53 are compared at that precision.
|
|
110
|
+
#
|
|
108
111
|
# @example
|
|
109
112
|
# e = CA_FLOAT64([0, 1, 10, 100])
|
|
110
113
|
# v = CA_FLOAT64([0.5, 5.0, 50.0, -1.0, 200.0])
|
|
@@ -140,7 +140,7 @@ class CArray
|
|
|
140
140
|
# consistency required across rows / columns).
|
|
141
141
|
def self.__ragged_paste (list, tdim, axis, data_type, name)
|
|
142
142
|
shared_attrs = attrs_shared_by(list)
|
|
143
|
-
list =
|
|
143
|
+
list = promote_list_for(name, list, data_type: data_type)
|
|
144
144
|
# promote_list has already enforced homogeneity (= common data_type, and
|
|
145
145
|
# for Face elements: same Face class + portable + state-compatible). So,
|
|
146
146
|
# mirroring CArray.stack, paste at the storage level and re-wrap the
|
|
@@ -44,6 +44,8 @@ class CArray
|
|
|
44
44
|
if excluded.count(true) > 0
|
|
45
45
|
codes.mask = codes.has_mask? ? (codes.mask | excluded) : excluded
|
|
46
46
|
end
|
|
47
|
+
# A const string's discovery reads its bytes and gives Ruby Strings.
|
|
48
|
+
levels = CArray.const_string(levels, encoding: encoding) if is_a?(CAConstString)
|
|
47
49
|
[codes, levels]
|
|
48
50
|
end
|
|
49
51
|
|
|
@@ -94,7 +94,10 @@ class CArray
|
|
|
94
94
|
# The write goes through a flattened view, which carries a frozen
|
|
95
95
|
# array's read-only flag but not its frozenness; say what the array is.
|
|
96
96
|
raise FrozenError.new("can't modify frozen #{self.class}", receiver: self) if frozen?
|
|
97
|
-
flat_addr,
|
|
97
|
+
flat_addr, outer, rest = gather_nd_flat_addr(indices, "put_nd")
|
|
98
|
+
# The values lie as gather_nd gives them back.
|
|
99
|
+
out_shape = outer + rest
|
|
100
|
+
flat_addr = flat_addr.reshape(*out_shape) unless out_shape.empty?
|
|
98
101
|
if flat_addr.has_mask?
|
|
99
102
|
# A masked coordinate writes nothing. The values are laid out over
|
|
100
103
|
# the coordinates by the store itself, so they mean what they mean
|
data/lib/carray/methods/is_in.rb
CHANGED
|
@@ -40,8 +40,9 @@ class CArray
|
|
|
40
40
|
# `Array#include?` does, so they are equal there. To match by value
|
|
41
41
|
# across Integer and Float, convert the array to a numeric type first.
|
|
42
42
|
#
|
|
43
|
-
# Masked cells of `values` do not enter the set
|
|
44
|
-
# `self`
|
|
43
|
+
# Masked cells of `values` do not enter the set, and neither does `nil`
|
|
44
|
+
# unless `self` is an `:object` array, which can hold it. Masked cells
|
|
45
|
+
# of `self` stay masked in the result (membership is unknown), so
|
|
45
46
|
# `is_in` propagates `self`'s mask like an element-wise comparison.
|
|
46
47
|
#
|
|
47
48
|
# For a per-fiber "does this fiber contain any of these values"
|
|
@@ -59,7 +60,7 @@ class CArray
|
|
|
59
60
|
# @return [CArray] boolean CArray of the same shape as `self`.
|
|
60
61
|
def is_in (values)
|
|
61
62
|
refuse_range_set(values, "is_in")
|
|
62
|
-
a, b = promote_value_set(values)
|
|
63
|
+
a, b = promote_value_set(values_of_set(values))
|
|
63
64
|
a.__send__(:__is_in__, b)
|
|
64
65
|
end
|
|
65
66
|
|
|
@@ -132,6 +133,25 @@ class CArray
|
|
|
132
133
|
"((#{values.inspect}).to_a), or test an interval with is_between(#{values.inspect})"
|
|
133
134
|
end
|
|
134
135
|
|
|
136
|
+
# The set as the array reads it. nil is no value of an array that can not
|
|
137
|
+
# hold it, so it enters no set, and true and false are the numbers 1 and 0
|
|
138
|
+
# to an integer, float or complex array, as they are to eq.
|
|
139
|
+
def values_of_set (values)
|
|
140
|
+
return values if data_type == CA_OBJECT
|
|
141
|
+
numeric = !face? && (integer? || float? || complex?)
|
|
142
|
+
case values
|
|
143
|
+
when Array
|
|
144
|
+
values = values.compact
|
|
145
|
+
numeric ? values.map { |v| (v == true) ? 1 : (v == false) ? 0 : v } : values
|
|
146
|
+
when CArray
|
|
147
|
+
( values.data_type == CA_OBJECT ) ? values.mask_eq(nil) : values
|
|
148
|
+
when true, false
|
|
149
|
+
numeric ? (values ? 1 : 0) : values
|
|
150
|
+
else
|
|
151
|
+
values
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
135
155
|
# Reconcile a set-valued argument with self to a common data_type via
|
|
136
156
|
# CArray.result_type (the single-source promotion rule the eager binop and
|
|
137
157
|
# lazy CABinOp share), returning [self', set']. Only data types are reconciled,
|
|
@@ -135,7 +135,9 @@ class CArray
|
|
|
135
135
|
# `self`; cells with no match in the direction and beyond-tolerance
|
|
136
136
|
# cells are masked.
|
|
137
137
|
# @raise [ArgumentError] when `direction` is not one of the accepted
|
|
138
|
-
# symbols,
|
|
138
|
+
# symbols, `tolerance` is not a duration for a time key, or `:round` /
|
|
139
|
+
# `tolerance` is asked of an object key whose values have no `-` (a
|
|
140
|
+
# String is ordered but has no distance).
|
|
139
141
|
def locate_nearest_addr (ref, direction: :round, tolerance: nil)
|
|
140
142
|
unless [:round, :floor, :ceil].include?(direction)
|
|
141
143
|
raise ArgumentError,
|
|
@@ -150,8 +152,8 @@ class CArray
|
|
|
150
152
|
# one has no place among the others either; it sorts after them.
|
|
151
153
|
ri = ref.sort_addr
|
|
152
154
|
ri = ri[0...ref.count_not_masked] if ref.has_mask?
|
|
153
|
-
if
|
|
154
|
-
ri = ri[0...(ri.elements -
|
|
155
|
+
if ri.elements > 0 && (nan = nearest_nan_cells(ref[ri]))
|
|
156
|
+
ri = ri[0...(ri.elements - nan.count(true))]
|
|
155
157
|
end
|
|
156
158
|
if ri.elements == 0
|
|
157
159
|
out = CArray.int64(*shape)
|
|
@@ -160,6 +162,7 @@ class CArray
|
|
|
160
162
|
end
|
|
161
163
|
rs = ref[ri]
|
|
162
164
|
n = rs.elements
|
|
165
|
+
nearest_refuse_distanceless(rs, direction, limit)
|
|
163
166
|
rk, qk = nearest_keys(rs, self)
|
|
164
167
|
# The position of each cell among the sorted references is counted, not
|
|
165
168
|
# interpolated: :floor takes the last reference at or below it, :ceil the
|
|
@@ -172,17 +175,69 @@ class CArray
|
|
|
172
175
|
when :round then nearest_round(rk, qk, nearest_count_below(rk, qk, false))
|
|
173
176
|
end
|
|
174
177
|
si[si.lt(0) | si.ge(n)] = UNDEF
|
|
175
|
-
|
|
178
|
+
nan = nearest_nan_cells(qk)
|
|
179
|
+
si[nan.strip_mask(false)] = UNDEF if nan
|
|
176
180
|
si = si.reshape(*shape)
|
|
177
181
|
si[is_masked] = UNDEF if has_mask?
|
|
178
182
|
idx = ri.project(si)
|
|
179
183
|
if limit
|
|
180
|
-
|
|
181
|
-
idx[dist > limit] = UNDEF
|
|
184
|
+
idx[nearest_beyond(ref.project(idx), self, limit)] = UNDEF
|
|
182
185
|
end
|
|
183
186
|
idx
|
|
184
187
|
end
|
|
185
188
|
|
|
189
|
+
# The NaN cells of a key, nil for a key that has none. A NaN in an object
|
|
190
|
+
# key is the cell that differs from itself.
|
|
191
|
+
private def nearest_nan_cells(key)
|
|
192
|
+
return key.is_nan if key.float?
|
|
193
|
+
return key.ne(key) if key.data_type == CA_OBJECT && !key.face?
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# :round and tolerance: measure how far a key is from its neighbours. An
|
|
198
|
+
# object key whose values have no subtraction (a String) is ordered but
|
|
199
|
+
# has no distance: only :floor and :ceil apply to it.
|
|
200
|
+
private def nearest_refuse_distanceless(key, direction, limit, subject = "locate_nearest_addr")
|
|
201
|
+
return unless key.data_type == CA_OBJECT && !key.face?
|
|
202
|
+
return unless direction == :round || limit
|
|
203
|
+
value = key.to_a.find { |v| !v.equal?(UNDEF) && v == v }
|
|
204
|
+
return if value.nil? || value.respond_to?(:-)
|
|
205
|
+
asked = (direction == :round) ? "direction: :round" : "tolerance:"
|
|
206
|
+
raise ArgumentError,
|
|
207
|
+
"#{subject}: #{asked} measures a distance, and a key of #{value.class} values " \
|
|
208
|
+
"has none (#{value.class} has no -); use direction: :floor or :ceil without tolerance:"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Whether each pair of keys lies farther apart than +limit+. A time key is
|
|
212
|
+
# measured in ticks of the unit both keys meet in, as an integer key is:
|
|
213
|
+
# two instants an int64 holds can lie farther apart than an int64 holds (a
|
|
214
|
+
# nanosecond key spanning more than 292 years).
|
|
215
|
+
private def nearest_beyond(a, b, limit)
|
|
216
|
+
time_key = (a.is_a?(CATime) || a.is_a?(CATimedelta)) && a.class == b.class
|
|
217
|
+
r = time_key && CATimeUnitAlgebra.ratio(limit.unit, CATimeUnitAlgebra.diff_unit(a.unit, b.unit))
|
|
218
|
+
return nearest_distance(a, b) > limit unless r
|
|
219
|
+
a, b = a.in_common_unit_with(b)
|
|
220
|
+
threshold = (limit.value * r).floor
|
|
221
|
+
out = nearest_distance(a.ticks, b.ticks) > [threshold, 2**64 - 1].min
|
|
222
|
+
out[] = false if threshold >= 2**64 - 1
|
|
223
|
+
out
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# How far apart two keys are. Integer keys are measured from the smaller to
|
|
227
|
+
# the larger, which fits an unsigned integer however far apart they are:
|
|
228
|
+
# the difference in their own type would wrap.
|
|
229
|
+
private def nearest_distance(a, b)
|
|
230
|
+
return (a - b).abs if a.face? || b.face?
|
|
231
|
+
t = a.send(:query_match_type, b)
|
|
232
|
+
return (a - b).abs if t == CA_OBJECT
|
|
233
|
+
a, b = a.to_type(t), b.to_type(t)
|
|
234
|
+
return (a - b).abs unless a.integer?
|
|
235
|
+
unless t == CA_UINT64
|
|
236
|
+
a, b = a.to_type(CA_INT64), b.to_type(CA_INT64)
|
|
237
|
+
end
|
|
238
|
+
(a.pmax(b) - a.pmin(b)).to_type(CA_UINT64)
|
|
239
|
+
end
|
|
240
|
+
|
|
186
241
|
# A key held as fixed-length bytes (raw fixlen, or a fixlen string) has
|
|
187
242
|
# no nearest: the search counts keys as numbers. A Face whose storage is
|
|
188
243
|
# a number (a time) is searched by that number.
|
|
@@ -207,11 +262,7 @@ class CArray
|
|
|
207
262
|
# that holds both.
|
|
208
263
|
private def nearest_keys(rs, q)
|
|
209
264
|
if rs.face? || q.face?
|
|
210
|
-
if rs.respond_to?(:to_unit) && q.class == rs.class && q.unit != rs.unit
|
|
211
|
-
u = CATimeUnitAlgebra.finer(rs.unit, q.unit)
|
|
212
|
-
rs = rs.to_unit(u)
|
|
213
|
-
q = q.to_unit(u)
|
|
214
|
-
end
|
|
265
|
+
rs, q = rs.in_common_unit_with(q) if rs.respond_to?(:to_unit) && q.class == rs.class && q.unit != rs.unit
|
|
215
266
|
q = rs.__send__(:reconcile_operand, q, "locate_nearest_addr")
|
|
216
267
|
rs = rs.__send__(:face_storage)
|
|
217
268
|
end
|
|
@@ -248,7 +299,7 @@ class CArray
|
|
|
248
299
|
private def nearest_round(rk, qk, lt)
|
|
249
300
|
lo = rk.project(lt - 1)
|
|
250
301
|
hi = rk.project(lt)
|
|
251
|
-
upper = (hi
|
|
302
|
+
upper = nearest_distance(hi, qk).le(nearest_distance(qk, lo)).strip_mask(false)
|
|
252
303
|
upper[lo.is_masked] = true if lo.has_mask?
|
|
253
304
|
upper[hi.is_masked] = false if hi.has_mask?
|
|
254
305
|
upper[hi.eq(qk).strip_mask(false)] = true
|