carray 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +520 -0
  3. data/README.md +2 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +33 -4
  6. data/ext/ca_axis_group.c +202 -96
  7. data/ext/ca_categorical_iterator.c +108 -54
  8. data/ext/ca_kernel_iterator.c +317 -51
  9. data/ext/ca_kernel_iterator.h +142 -35
  10. data/ext/ca_obj_array.c +62 -20
  11. data/ext/ca_obj_block.c +4 -4
  12. data/ext/ca_obj_const_string.c +85 -26
  13. data/ext/ca_obj_face.c +24 -0
  14. data/ext/ca_obj_face.h +15 -0
  15. data/ext/ca_obj_fixlen_string.c +18 -5
  16. data/ext/ca_obj_meld.c +123 -25
  17. data/ext/ca_obj_object.c +8 -0
  18. data/ext/ca_obj_select.c +49 -34
  19. data/ext/ca_obj_stack.c +3 -8
  20. data/ext/ca_obj_stride.c +72 -1
  21. data/ext/ca_obj_string.c +8 -4
  22. data/ext/ca_obj_window.c +8 -2
  23. data/ext/ca_op_ipower.c +1 -2
  24. data/ext/ca_rng_normal.h +42 -0
  25. data/ext/ca_rng_xoshiro256pp.h +105 -0
  26. data/ext/ca_sweep_engine.c +307 -143
  27. data/ext/ca_sweep_engine.h +26 -5
  28. data/ext/carray.h +21 -2
  29. data/ext/carray_access.c +32 -20
  30. data/ext/carray_address_basis.c +590 -0
  31. data/ext/carray_broadcast.c +3 -3
  32. data/ext/carray_call_cfunc.c +667 -483
  33. data/ext/carray_cast.c +115 -41
  34. data/ext/carray_copy.c +55 -30
  35. data/ext/carray_core.c +83 -3
  36. data/ext/carray_count.c +9 -10
  37. data/ext/carray_factorize.c +46 -25
  38. data/ext/carray_internal.h +17 -0
  39. data/ext/carray_kernels_reduce_aggregate.c +168 -0
  40. data/ext/carray_kernels_reduce_cumulative.c +270 -1
  41. data/ext/carray_kernels_reduce_extreme.c +554 -8
  42. data/ext/carray_kernels_scan.c +4 -4
  43. data/ext/carray_kernels_search.c +94 -14
  44. data/ext/carray_loop.c +7 -1
  45. data/ext/carray_mask.c +23 -8
  46. data/ext/carray_median_percentile.c +55 -0
  47. data/ext/carray_operator.c +4 -4
  48. data/ext/carray_order.c +1 -1
  49. data/ext/carray_random.c +384 -40
  50. data/ext/carray_slab.c +13 -0
  51. data/ext/carray_sort.c +20 -22
  52. data/ext/mk_call_cfunc.rb +103 -116
  53. data/ext/mkkernel.rb +297 -29
  54. data/ext/ruby_carray.c +10 -1
  55. data/ext/version.h +4 -4
  56. data/lib/carray/autoload_carray.rb +5 -3
  57. data/lib/carray/autoload_method_extension.rb +12 -0
  58. data/lib/carray/axis_group.rb +77 -0
  59. data/lib/carray/basics.rb +4 -0
  60. data/lib/carray/block_iterator.rb +92 -16
  61. data/lib/carray/categorical.rb +150 -33
  62. data/lib/carray/categorical_iterator.rb +207 -80
  63. data/lib/carray/const_string.rb +131 -27
  64. data/lib/carray/construct.rb +40 -0
  65. data/lib/carray/data_type_extension.rb +3 -0
  66. data/lib/carray/data_type_limits.rb +91 -0
  67. data/lib/carray/fixlen_string.rb +1 -1
  68. data/lib/carray/frame/csv_parser.rb +11 -4
  69. data/lib/carray/frame/frame.rb +81 -10
  70. data/lib/carray/frame/group.rb +36 -3
  71. data/lib/carray/frame/io.rb +67 -15
  72. data/lib/carray/frame/records.rb +18 -4
  73. data/lib/carray/frame/verbs.rb +14 -11
  74. data/lib/carray/inspect.rb +42 -9
  75. data/lib/carray/iterator.rb +143 -0
  76. data/lib/carray/lazy.rb +0 -37
  77. data/lib/carray/mask_gap_fill.rb +3 -1
  78. data/lib/carray/methods/discovery_along.rb +74 -0
  79. data/lib/carray/methods/factorize.rb +50 -0
  80. data/lib/carray/methods/is_in.rb +13 -2
  81. data/lib/carray/methods/locate_addr.rb +75 -2
  82. data/lib/carray/methods/mask_duplicates.rb +35 -1
  83. data/lib/carray/methods/nunique.rb +22 -1
  84. data/lib/carray/methods/repeat.rb +110 -0
  85. data/lib/carray/methods/unique.rb +41 -1
  86. data/lib/carray/rng.rb +86 -0
  87. data/lib/carray/slab_iterator.rb +58 -13
  88. data/lib/carray/string_operation_extension.rb +5 -1
  89. data/lib/carray/time.rb +18 -2
  90. data/lib/carray/window_iterator.rb +142 -20
  91. data/lib/carray.rb +2 -0
  92. data/yard-stubs/ca_obj_block.rb +2 -7
  93. data/yard-stubs/ca_obj_window.rb +10 -2
  94. data/yard-stubs/carray_access.rb +1 -1
  95. data/yard-stubs/carray_broadcast.rb +1 -1
  96. data/yard-stubs/carray_core.rb +0 -80
  97. data/yard-stubs/carray_count.rb +7 -2
  98. data/yard-stubs/carray_lazy.rb +205 -0
  99. data/yard-stubs/carray_math.rb +1486 -3
  100. data/yard-stubs/carray_median_percentile.rb +16 -2
  101. data/yard-stubs/carray_order.rb +9 -69
  102. data/yard-stubs/carray_slab.rb +9 -7
  103. data/yard-stubs/carray_sort.rb +7 -5
  104. metadata +9 -1
@@ -11,12 +11,27 @@ class CAFrame
11
11
  # later. Broken cells fail to_type and become UNDEF automatically
12
12
  # (parse-mask, §6-2).
13
13
  #
14
+ # +source+ is a path, or an open IO -- anything answering +gets+, which a
15
+ # StringIO is. So CSV that is already in memory does not have to go to a
16
+ # temporary file first:
17
+ #
18
+ # CAFrame.from_csv("obs.csv") # a path
19
+ # CAFrame.from_csv(StringIO.new(body)) # text already in hand
20
+ # File.open("obs.csv") { |io| CAFrame.from_csv(io) }
21
+ #
22
+ # A String is always read as a path, never as CSV text: guessing between the
23
+ # two by looking for a newline is the kind of guess that is right until it is
24
+ # not, and StringIO says which one you meant in six characters. An IO is read
25
+ # from where it is and left open -- the caller opened it and closes it.
26
+ #
14
27
  # Parsing uses the built-in fast tokenizer (CSVParser). Options:
15
28
  # sep: field separator (default ",")
16
29
  # quote: quote character (default '"')
17
30
  # strip: trim spaces from unquoted fields (default false, RFC spacing)
18
- # +encoding+: IO open-mode encoding (default "bom|utf-8", strips a BOM)
19
- # parser: a callable path -> [headers, rows] to inject another parser
31
+ # +encoding+: open-mode encoding for a path (default "bom|utf-8", strips a
32
+ # BOM). It has nothing to open when +source+ is an IO, so there
33
+ # the IO's own encoding governs and a BOM is the caller's.
34
+ # parser: a callable source -> [headers, rows] to inject another parser
20
35
  # (e.g. the stdlib +csv+, or a typed-table source); when given,
21
36
  # sep/quote/strip/encoding and any block are that parser's concern.
22
37
  #
@@ -28,25 +43,25 @@ class CAFrame
28
43
  # skip 2; header; skip 1; body
29
44
  # end
30
45
  #
46
+ # A missing field -- an unquoted empty one, or a cell a short row never
47
+ # reached -- is UNDEF in the frame, whether or not the column is cast by
48
+ # +types:+. A quoted empty field ("") is the empty string, which is a value.
49
+ # So the mask +to_csv+ writes comes back as a mask.
50
+ #
31
51
  # Columns are handed to the frame as CABlock views over one backing object
32
52
  # array (§3.6 view-by-default); casting a column materializes it, and +copy+
33
53
  # gives an independent frame.
34
- def self.from_csv(path, types: nil,
54
+ def self.from_csv(source, types: nil,
35
55
  sep: ",", quote: '"', strip: false,
36
56
  encoding: "bom|utf-8", parser: nil, &block)
37
57
  names, rows =
38
58
  if parser
39
- parser.call(path)
59
+ parser.call(source)
60
+ elsif source.respond_to?(:gets)
61
+ read_csv(source, sep: sep, quote: quote, strip: strip, &block)
40
62
  else
41
- File.open(path, "r:#{encoding}") do |io|
42
- reader = CSVReader.new(io, sep: sep, quote: quote, strip: strip)
43
- if block
44
- block.arity == 1 ? block.call(reader) : reader.instance_exec(&block)
45
- else
46
- reader.header
47
- reader.body
48
- end
49
- reader.result
63
+ File.open(source, "r:#{encoding}") do |io|
64
+ read_csv(io, sep: sep, quote: quote, strip: strip, &block)
50
65
  end
51
66
  end
52
67
 
@@ -55,6 +70,22 @@ class CAFrame
55
70
  frame
56
71
  end
57
72
 
73
+ # Drive the reading-control DSL over one open IO and hand back
74
+ # [names, rows]. Shared by the path and the IO source, so the two cannot
75
+ # come to read a file differently.
76
+ def self.read_csv (io, sep:, quote:, strip:, &block)
77
+ reader = CSVReader.new(io, sep: sep, quote: quote, strip: strip)
78
+ if block
79
+ block.arity == 1 ? block.call(reader) : reader.instance_exec(&block)
80
+ else
81
+ reader.header
82
+ reader.body
83
+ end
84
+ reader.result
85
+ end
86
+
87
+ private_class_method :read_csv
88
+
58
89
  # Build a frame from parsed [names, rows]. When names is nil (headerless and
59
90
  # no column_names) positional names "c0".."cN" are generated from the widest
60
91
  # row. Rows are squared off to the column count (short rows padded with nil,
@@ -159,7 +190,9 @@ class CAFrame
159
190
  parts = @columns.map { |k, v| "#{k}:#{v.data_type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}" }
160
191
  idx = @index ? " index=#{@axis_name.inspect}" : ""
161
192
  head = "#<CAFrame nrow=#{@nrow} vars=[#{parts.join(', ')}]#{idx}>"
162
- return head if @columns.empty?
193
+ # The table counts the index as a column, so a frame whose only data is its
194
+ # index has one to show. Gate on the same thing render_table does.
195
+ return head if @columns.empty? && @index.nil?
163
196
  head + "\n" + render_table(head: 8, tail: 2, index: true, precision: 6,
164
197
  footer: false)
165
198
  end
@@ -252,6 +285,15 @@ class CAFrame
252
285
  "#{n} #{noun}#{n == 1 ? '' : 's'}"
253
286
  end
254
287
 
288
+ # Render one N-D cell the way Array#inspect would, except that a masked
289
+ # element prints as the table's missing marker rather than as UNDEF.
290
+ private def format_nested_cell(v)
291
+ case v
292
+ when Array then "[" + v.map { |x| format_nested_cell(x) }.join(", ") + "]"
293
+ else UNDEF.equal?(v) || v.nil? ? "_" : v.inspect
294
+ end
295
+ end
296
+
255
297
  private def format_table_cell(col, i, precision)
256
298
  e = elem_at(col, i)
257
299
  if UNDEF.equal?(e) || e.nil?
@@ -262,7 +304,10 @@ class CAFrame
262
304
  # to read. precision: nil prints the value as Ruby renders it.
263
305
  e.round(precision).to_s
264
306
  elsif e.is_a?(CArray)
265
- e.to_a.inspect
307
+ # An N-D cell renders its elements, and a masked element among them
308
+ # takes the same marker a masked scalar does -- UNDEF's own inspect
309
+ # would put a second spelling of "missing" in the same table.
310
+ format_nested_cell(e.to_a)
266
311
  elsif e.is_a?(String)
267
312
  e
268
313
  elsif e.respond_to?(:iso8601)
@@ -314,6 +359,13 @@ class CAFrame
314
359
  end
315
360
  end
316
361
  table = CArray.object(rows.size, ncol) { rows }
362
+ # A missing field is UNDEF, not a Ruby nil sitting in a cell. The
363
+ # tokenizer says missing with nil (an unquoted empty field; a quoted
364
+ # one is the empty string and stays a value), and an object array will
365
+ # hold that nil quite happily -- so a column read without `types:` used
366
+ # to carry nil where the same column read with one carried UNDEF, and
367
+ # the mask a to_csv had written did not survive the trip back.
368
+ table[:eq, nil] = UNDEF
317
369
  names.each_with_index { |name, j| cols[name] = table[nil, j] }
318
370
  new(cols)
319
371
  end
@@ -59,30 +59,44 @@ class CAFrame
59
59
  # native scalar column (nil -> UNDEF); anything else -> object column.
60
60
  def self.build_record_column(values, n)
61
61
  present = values.reject(&:nil?)
62
- return CArray.object(n) { values } if present.empty?
62
+ return mask_missing(CArray.object(n) { values }) if present.empty?
63
63
 
64
64
  if present.all? { |v| v.is_a?(Array) || v.is_a?(CArray) }
65
65
  build_nd_column(values, present, n)
66
66
  else
67
67
  type = numeric_leaf_type(present)
68
68
  col = CArray.object(n) { values }
69
- type ? col.to_type(type) : col
69
+ type ? col.to_type(type) : mask_missing(col)
70
70
  end
71
71
  end
72
72
  private_class_method :build_record_column
73
73
 
74
+ # A missing cell is UNDEF, not a Ruby nil sitting in a cell. to_records
75
+ # writes a masked cell as nil, so nil on the way back in is the only
76
+ # spelling a missing cell has; to_type does this conversion for a numeric
77
+ # column, and an object column would otherwise keep the nil as a value --
78
+ # a row with no label coming back as a row labelled nil. The CSV reader
79
+ # takes the same position (see build_frame in io.rb).
80
+ def self.mask_missing(col)
81
+ col[:eq, nil] = UNDEF
82
+ col
83
+ end
84
+ private_class_method :mask_missing
85
+
74
86
  # Stack equal-length array cells into an (N, L) column via an object 2-D fill
75
87
  # + to_type (nil rows -> UNDEF, int/float by leaf). Ragged lengths or
76
88
  # non-numeric leaves fall back to a 1-D object column of the raw cells.
77
89
  def self.build_nd_column(values, present, n)
78
90
  lengths = present.map { |v| v.is_a?(CArray) ? v.shape[0] : v.size }
79
91
  len = lengths.first
80
- return CArray.object(n) { values } unless lengths.all? { |x| x == len }
92
+ unless lengths.all? { |x| x == len }
93
+ return mask_missing(CArray.object(n) { values })
94
+ end
81
95
 
82
96
  nested = values.map { |v| v.nil? ? Array.new(len) : (v.is_a?(CArray) ? v.to_a : v) }
83
97
  type = numeric_leaf_type(nested.flatten.compact)
84
98
  table = CArray.object(n, len) { nested }
85
- type ? table.to_type(type) : table
99
+ type ? table.to_type(type) : mask_missing(table)
86
100
  end
87
101
  private_class_method :build_nd_column
88
102
 
@@ -230,7 +230,9 @@ class CAFrame
230
230
  return
231
231
  end
232
232
  yvalid = col.value.float64[present]
233
- col[] = yvalid.linear_fetch(addr).to_type(col.data_type).mask_invalid # write-through
233
+ # mask_invalid before the cast: linear_fetch marks out of range with NaN,
234
+ # and casting back to an integer column would turn it into a plausible 0.
235
+ col[] = yvalid.linear_fetch(addr).mask_invalid.to_type(col.data_type) # write-through
234
236
  end
235
237
 
236
238
  private def cast_one(name, type)
@@ -267,16 +269,17 @@ class CAFrame
267
269
  "promote takes a data type Symbol (got #{type.class}); " \
268
270
  "class-shaped targets are not promotion destinations"
269
271
  end
270
- @columns.each_key do |key|
271
- col = @columns[key]
272
- # A Face column answers for itself: :object is its surface values, a
273
- # numeric target is whatever it declares in #to_numeric (and a TypeError
274
- # naming that method when it declares nothing). result_type has nothing
275
- # to say about a surface it cannot read, so the widening check -- which
276
- # is about primitive promotion -- applies to plain columns only.
277
- refuse_narrowing(key, col, type) unless col.face?
278
- @columns[key] = col.to_type(type)
279
- end
272
+ # Check every column before rebinding any. A column that would narrow
273
+ # rejects the whole promote, and rejecting part way through would leave the
274
+ # frame promoted in whichever columns happened to come first.
275
+ #
276
+ # A Face column answers for itself: :object is its surface values, a
277
+ # numeric target is whatever it declares in #to_numeric (and a TypeError
278
+ # naming that method when it declares nothing). result_type has nothing
279
+ # to say about a surface it cannot read, so the widening check -- which
280
+ # is about primitive promotion -- applies to plain columns only.
281
+ @columns.each { |key, col| refuse_narrowing(key, col, type) unless col.face? }
282
+ @columns.each_key { |key| @columns[key] = @columns[key].to_type(type) }
280
283
  end
281
284
 
282
285
  private def refuse_narrowing(key, col, type)
@@ -7,7 +7,12 @@ class CArray::Inspector # :nodoc:
7
7
  end
8
8
 
9
9
  # @!visibility private
10
- def inspect_string
10
+ #
11
+ # `abbrev` false renders every element instead of eliding with `...`.
12
+ # It is the one difference between #inspect and #inspect_full: the
13
+ # header and the layout are the same, so there is one renderer rather
14
+ # than two that could drift apart.
15
+ def inspect_string (abbrev: true)
11
16
  if @carray.ndim == 0
12
17
  raise "can't inspect CArray of ndim == 0"
13
18
  end
@@ -15,7 +20,7 @@ class CArray::Inspector # :nodoc:
15
20
  class_name = get_class_name()
16
21
  type_name = get_type_name()
17
22
  shape = get_shape()
18
- data_spec = get_data_spec(0, Array.new(@carray.ndim){0}, formatter)
23
+ data_spec = get_data_spec(0, Array.new(@carray.ndim){0}, formatter, abbrev)
19
24
  info_list = get_info_list()
20
25
  output = ["<",
21
26
  format("%s.%s(%s)", class_name, type_name, shape.join(",")),
@@ -146,7 +151,7 @@ class CArray::Inspector # :nodoc:
146
151
  end
147
152
  end
148
153
 
149
- def get_data_spec (level, idx, formatter)
154
+ def get_data_spec (level, idx, formatter, abbrev = true)
150
155
  io = +"[ " # mutable buffer; `<<` below appends into it
151
156
  ndim = @carray.ndim
152
157
  dim = @carray.shape
@@ -163,7 +168,7 @@ class CArray::Inspector # :nodoc:
163
168
  if i != dim[level] - 1
164
169
  io << ", "
165
170
  end
166
- if io.length > 48 - 2*level
171
+ if abbrev and io.length > 48 - 2*level
167
172
  if i < dim[level] - 1
168
173
  io << "..."
169
174
  over = true
@@ -182,24 +187,24 @@ class CArray::Inspector # :nodoc:
182
187
  end
183
188
  else
184
189
  over = false
185
- show = [dim[level], 5].min
190
+ show = abbrev ? [dim[level], 5].min : dim[level]
186
191
  show.times do |i|
187
192
  idx[level] = i
188
- io << get_data_spec(level+1, idx, formatter)
193
+ io << get_data_spec(level+1, idx, formatter, abbrev)
189
194
  if i < show - 1
190
195
  io << ",\n" + " " * (level+1)
191
196
  end
192
- if i >= 2 and dim[level] > 5
197
+ if abbrev and i >= 2 and dim[level] > 5
193
198
  break
194
199
  end
195
200
  end
196
- if dim[level] > 5
201
+ if abbrev and dim[level] > 5
197
202
  io << "... ... ..."
198
203
  over = true
199
204
  end
200
205
  if over
201
206
  idx[level] = dim[level] - 1
202
- io << "\n"+ " " * (level+1) + get_data_spec(level+1, idx, formatter)
207
+ io << "\n"+ " " * (level+1) + get_data_spec(level+1, idx, formatter, abbrev)
203
208
  end
204
209
  end
205
210
  io << " ]"
@@ -219,6 +224,34 @@ class CArray
219
224
  return CArray::Inspector.new(self).inspect_string
220
225
  end
221
226
 
227
+ # @overload inspect_full
228
+ # The same description as {#inspect}, with every element rendered
229
+ # instead of the `...` preview.
230
+ #
231
+ # `inspect` abbreviates on purpose -- it is what `p`, `irb` and an
232
+ # error message call, and a million-cell array has to stay readable
233
+ # there. `inspect_full` is for the other moment, when the whole
234
+ # array is the thing you came to look at:
235
+ #
236
+ # puts a.inspect_full
237
+ #
238
+ # The header, the layout and the `_` for a masked cell are
239
+ # `inspect`'s; only the eliding is dropped, so for an array small
240
+ # enough that `inspect` was not abbreviating anything the two give
241
+ # the same string.
242
+ #
243
+ # Note that neither of these is `to_s`, which returns the **raw
244
+ # bytes** of the storage rather than anything printable.
245
+ #
246
+ # The result is one String holding every element, so it is as large
247
+ # as the array is: nothing here is streamed, and a line is as long
248
+ # as the last axis makes it.
249
+ #
250
+ # @return [String]
251
+ def inspect_full
252
+ return CArray::Inspector.new(self).inspect_string(abbrev: false)
253
+ end
254
+
222
255
  private
223
256
 
224
257
  def desc
@@ -36,6 +36,114 @@ class CAIterator
36
36
  # rather than reading as "no such method" -- that gap is the member's to close.
37
37
  # A member that genuinely cannot provide one overrides it to raise with its
38
38
  # own reason.
39
+ # The methods below are declared here, so that the family surface is
40
+ # documented in one place; each member generates its own implementation and
41
+ # a member for which one is ill-defined overrides it to raise with a reason.
42
+ # "Piece" is the unit a member iterates over: a slab, a window, a block, a
43
+ # category, or a group.
44
+ #
45
+ # @!method sum
46
+ # Returns the sum of each piece.
47
+ # @return [CArray] one value per piece, shaped {#shape}.
48
+ # @!method accumulate
49
+ # Returns the sum of each piece kept in the source's own `data_type`,
50
+ # wrapping at its width, where {#sum} answers in the type the core
51
+ # promotes to (`:float64` for integers).
52
+ # @return [CArray] one value per piece.
53
+ # @!method prod
54
+ # Returns the product of each piece.
55
+ # @return [CArray] one value per piece.
56
+ # @!method mean
57
+ # Returns the arithmetic mean of each piece.
58
+ # @return [CArray] one value per piece.
59
+ # @!method min
60
+ # Returns the smallest value in each piece.
61
+ # @return [CArray] one value per piece.
62
+ # @!method max
63
+ # Returns the largest value in each piece.
64
+ # @return [CArray] one value per piece.
65
+ # @!method minmax
66
+ # Returns the smallest and largest value of each piece, found in one pass.
67
+ # @return [Array<CArray>] the pair `[min, max]`.
68
+ # @!method variance
69
+ # Returns the sample variance (divisor `n - 1`) of each piece.
70
+ # @return [CArray] one value per piece.
71
+ # @!method variancep
72
+ # Returns the population variance (divisor `n`) of each piece.
73
+ # @return [CArray] one value per piece.
74
+ # @!method stddev
75
+ # Returns the sample standard deviation (divisor `n - 1`) of each piece.
76
+ # @return [CArray] one value per piece.
77
+ # @!method stddevp
78
+ # Returns the population standard deviation (divisor `n`) of each piece.
79
+ # @return [CArray] one value per piece.
80
+ # @!method all
81
+ # Returns whether every cell of each piece is true.
82
+ # @return [CArray] `:boolean`, one value per piece.
83
+ # @!method any
84
+ # Returns whether any cell of each piece is true.
85
+ # @return [CArray] `:boolean`, one value per piece.
86
+ # @!method min_index
87
+ # Returns the position of the smallest value **within** each piece.
88
+ # @return [CArray] one index per piece.
89
+ # @!method max_index
90
+ # Returns the position of the largest value within each piece.
91
+ # @return [CArray] one index per piece.
92
+ # @!method min_addr
93
+ # Returns the flat address **in the source** of the smallest value of each
94
+ # piece -- which source cell holds it, rather than where it sits inside the
95
+ # piece. Use it to read the same cell out of another source-shaped array.
96
+ # @return [CArray] one flat address per piece.
97
+ # @!method max_addr
98
+ # Returns the flat address in the source of the largest value of each piece.
99
+ # @return [CArray] one flat address per piece.
100
+ # @!method wsum(weights)
101
+ # Returns the weighted sum of each piece.
102
+ # @param weights [CArray] one weight per source cell, shaped like the source.
103
+ # @return [CArray] one value per piece.
104
+ # @!method wmean(weights)
105
+ # Returns the weighted mean of each piece.
106
+ # @param weights [CArray] one weight per source cell, shaped like the source.
107
+ # @return [CArray] one value per piece.
108
+ # @!method median
109
+ # Returns the median of each piece.
110
+ # @return [CArray] one value per piece.
111
+ # @!method percentile(*pers)
112
+ # Returns the requested percentile(s) of each piece.
113
+ # @param pers [Array<Numeric>] percentile positions in `0..100`.
114
+ # @return [CArray, Array<CArray>] one array per requested position; a
115
+ # single position returns that array directly.
116
+ # @!method quantile
117
+ # Returns the five-number summary of each piece,
118
+ # `[min, Q1, median, Q3, max]`.
119
+ # @return [Array<CArray>] five arrays, one per position.
120
+ # @!method count(value = nil)
121
+ # Returns a count per piece: with no argument the cells that are not
122
+ # masked, with `UNDEF` the masked cells, and with any other value the
123
+ # cells equal to it.
124
+ # @param value [Object] value to match, or `UNDEF`.
125
+ # @return [CArray] one count per piece.
126
+ # @!method count_not_masked
127
+ # Returns the number of cells of each piece that are not masked.
128
+ # @return [CArray] one count per piece.
129
+ # @!method count_masked
130
+ # Returns the number of masked cells of each piece.
131
+ # @return [CArray] one count per piece.
132
+ # @!method elements
133
+ # Returns the total number of cells in each piece, masked or not.
134
+ # @return [CArray] one count per piece.
135
+ # @!method each
136
+ # Yields each piece in turn as a CArray.
137
+ # @yieldparam piece [CArray]
138
+ # @return [Enumerator, self] an Enumerator when no block is given.
139
+ # @!method reduce(init = nil)
140
+ # Folds the pieces with a block, for a reduction the family does not name.
141
+ # Without `init` the first piece seeds the accumulator.
142
+ # @param init [Object] initial accumulator value.
143
+ # @yieldparam acc [Object] running accumulator.
144
+ # @yieldparam piece [CArray] next piece.
145
+ # @yieldreturn [Object] updated accumulator.
146
+ # @return [Object] the final accumulator.
39
147
  [
40
148
  :sum, :accumulate, :prod, :mean, :min, :max, # tier 1
41
149
  :variance, :stddev, :all, :any,
@@ -66,6 +174,41 @@ class CAIterator
66
174
  # sort_addr. Un-overridden each is simply unavailable, not a contract
67
175
  # violation. min_addr / max_addr stay required: a single winner address is
68
176
  # well-defined even for an overlapping window.
177
+ # @!method map
178
+ # Returns a source-shaped array built by applying the block to each piece
179
+ # and scattering the result back into that piece's cells.
180
+ # @yieldparam piece [CArray]
181
+ # @yieldreturn [CArray, Numeric] replacement values for the piece.
182
+ # @return [CArray] shaped like the source.
183
+ # @raise [NotImplementedError] for a member whose pieces overlap, where a
184
+ # cell would receive more than one value.
185
+ # @!method sort_addr
186
+ # Returns a source-shaped array whose cells, read piece by piece, give the
187
+ # flat source addresses that put that piece in ascending order.
188
+ # @return [CArray] `:int64`, shaped like the source.
189
+ # @raise [NotImplementedError] for a member whose pieces overlap.
190
+ # @!method cumsum
191
+ # Returns a source-shaped array of the running sum within each piece.
192
+ # @return [CArray] shaped like the source.
193
+ # @raise [NotImplementedError] for a member whose pieces overlap, where a
194
+ # cell has no single running value.
195
+ # @!method cumprod
196
+ # Returns a source-shaped array of the running product within each piece.
197
+ # @return [CArray] shaped like the source.
198
+ # @raise [NotImplementedError] for a member whose pieces overlap.
199
+ # @!method cummax
200
+ # Returns a source-shaped array of the running maximum within each piece.
201
+ # @return [CArray] shaped like the source.
202
+ # @raise [NotImplementedError] for a member whose pieces overlap.
203
+ # @!method cummin
204
+ # Returns a source-shaped array of the running minimum within each piece.
205
+ # @return [CArray] shaped like the source.
206
+ # @raise [NotImplementedError] for a member whose pieces overlap.
207
+ # @!method cumcount
208
+ # Returns a source-shaped array of the running count of cells that are not
209
+ # masked within each piece.
210
+ # @return [CArray] shaped like the source.
211
+ # @raise [NotImplementedError] for a member whose pieces overlap.
69
212
  [:map, :sort_addr,
70
213
  :cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |name|
71
214
  define_method(name) do |*, **, &_blk|
data/lib/carray/lazy.rb CHANGED
@@ -901,41 +901,4 @@ class << CArray
901
901
  end
902
902
  CArray::FuseSource.evaluate(block)
903
903
  end
904
-
905
- # @overload jit_for (*extents) { |i, j, ...| ... }
906
- # Runs a block over an index space, so that a cell may reach the ones
907
- # around it -- a recurrence, a stencil. The block is compiled, and the
908
- # compiler is the carray-jit gem.
909
- # @raise [NotImplementedError] when that gem is not installed.
910
- def jit_for (*extents, **options)
911
- raise NotImplementedError, no_compiler("jit_for")
912
- end
913
-
914
- # @overload jit_each { ... }
915
- # Runs a block over the cells of arrays at once, naming each cell by the
916
- # array it came from, and writing what it computes into arrays of yours:
917
- # `CArray.jit_each { out = a + b * c }`. The block is compiled, and the
918
- # compiler is the carray-jit gem.
919
- # @raise [NotImplementedError] when that gem is not installed.
920
- def jit_each (&block)
921
- raise NotImplementedError, no_compiler("jit_each")
922
- end
923
-
924
- # @overload jit_map { ... }
925
- # The same block with its value asked for: the last statement is what
926
- # every cell of the result gets, and the result comes back. The block is
927
- # compiled, and the compiler is the carray-jit gem.
928
- # @raise [NotImplementedError] when that gem is not installed.
929
- def jit_map (&block)
930
- raise NotImplementedError, no_compiler("jit_map")
931
- end
932
-
933
- private
934
-
935
- def no_compiler (name)
936
- "CArray.#{name} compiles its block, and the compiler is the carray-jit " \
937
- "gem, which is not installed. An expression over whole arrays can be " \
938
- "written as `CArray.fuse { ... }` instead, which needs no compiler; " \
939
- "reaching a neighbouring cell, or writing the loop itself, does."
940
- end
941
904
  end
@@ -172,7 +172,9 @@ class CArray
172
172
  return vec[present].linear_fetch(addr)
173
173
  end
174
174
  vval = vec.value.float64[present] # valid values
175
- vval.linear_fetch(addr).to_type(vec.data_type).mask_invalid
175
+ # Mark the out-of-range NaN before casting back: for an integer data_type
176
+ # the cast turns NaN into 0, and mask_invalid then has nothing left to find.
177
+ vval.linear_fetch(addr).mask_invalid.to_type(vec.data_type)
176
178
  end
177
179
 
178
180
  # Yield an index key (Array with `nil` at `axis`, integers elsewhere)
@@ -0,0 +1,74 @@
1
+ class CArray
2
+
3
+ # The value-hash discovery family (`unique`, `nunique`,
4
+ # `mask_duplicates`, ...) interns one **cell** at a time. `along:`
5
+ # widens the unit to a whole sub-array: the sub-arrays enumerated
6
+ # along one axis, each compared as a single value.
7
+ #
8
+ # Nothing new has to hash. `fz_hash`'s third key lane already interns
9
+ # a fixed-width block of bytes (FNV-1a, with a memcmp re-check), which
10
+ # is what a sub-array is once it sits contiguously; this method is the
11
+ # bridge to it, and the three things the bridge has to get right:
12
+ #
13
+ # - the sub-arrays have to be contiguous before their bytes mean
14
+ # anything, which a transpose or a strided view does not give;
15
+ # - byte equality is not the family's contract for floats, where
16
+ # every NaN is one value and -0.0 is +0.0;
17
+ # - masks fold for free -- a CARefer that covers several parent
18
+ # cells with one view cell OR-reduces their mask bits, so a
19
+ # sub-array holding a masked cell is a masked sub-array and the
20
+ # kernels skip it.
21
+ #
22
+ # Returns a 1-D fixlen array, one cell per sub-array, for the caller
23
+ # to run an ordinary cell-level discovery method over.
24
+ private def fibers_as_cells (along, caller_name)
25
+ if ndim < 2
26
+ raise ArgumentError,
27
+ "#{caller_name}: along: compares sub-arrays, and a #{ndim}-D array " \
28
+ "has none -- drop along: for the whole-array form"
29
+ end
30
+
31
+ if data_type == CA_OBJECT
32
+ raise ArgumentError,
33
+ "#{caller_name}: along: is not available for an object array -- " \
34
+ "its cells hold Ruby references, which would compare by identity " \
35
+ "rather than by value"
36
+ end
37
+
38
+ axis = normalize_axis(along, caller_name)
39
+
40
+ # Bring the named axis to the front so each sub-array is one
41
+ # unbroken run of bytes, and take a copy: `refer` reinterprets a
42
+ # byte buffer, and a view whose element stride is not its element
43
+ # width is not one. The copy is also what makes the normalisation
44
+ # below safe to write.
45
+ order = [axis] + (0...ndim).to_a.reject { |i| i == axis }
46
+ block = (axis.zero? ? self : transpose(*order)).copy
47
+
48
+ # Two values the family calls equal are not equal byte for byte.
49
+ # Normalise them so the bytes say what the values mean.
50
+ if block.float?
51
+ block[:is_nan] = Float::NAN # every NaN is one value
52
+ block[:eq, 0.0] = 0.0 # -0.0 is +0.0
53
+ elsif block.complex?
54
+ [block.real, block.imag].each do |part|
55
+ part[:is_nan] = Float::NAN
56
+ part[:eq, 0.0] = 0.0
57
+ end
58
+ end
59
+
60
+ count = shape[axis]
61
+ block.refer(:fixlen, [count], bytes: elements / count * bytes)
62
+ end
63
+
64
+ # `axis:` and `along:` ask different questions of the same array, so
65
+ # answering both at once has no meaning.
66
+ private def reject_axis_with_along (axis, along, caller_name)
67
+ return unless axis and along
68
+ raise ArgumentError,
69
+ "#{caller_name}: axis: and along: cannot be given together -- " \
70
+ "axis: is about the values inside each sub-array, along: is about " \
71
+ "the sub-arrays themselves"
72
+ end
73
+
74
+ end
@@ -0,0 +1,50 @@
1
+ class CArray
2
+
3
+ # @overload factorize
4
+ # Returns `[codes, levels]` in one pass: `levels` is a 1-D CArray of
5
+ # the distinct values of `self` in first-appearance order — what
6
+ # {#unique} answers — and `codes` is an integer CArray of `self`'s
7
+ # shape where `levels[codes[i]]` is `self[i]`.
8
+ #
9
+ # This is the member of the value-hash discovery family ({#unique},
10
+ # {#value_counts}, {#mask_duplicates}, {#nunique}) that hands back
11
+ # the codes as storage. {#unique} answers with the vocabulary alone
12
+ # and {#categorize} wraps both in a {CACategorical} Face; a caller
13
+ # who wants the codes themselves — a position to scatter into, a key
14
+ # to group by, a dense renumbering of sparse keys — would otherwise
15
+ # pay a second pass or take the Face and its Ruby label list.
16
+ #
17
+ # `codes` takes the narrowest unsigned data type the vocabulary
18
+ # fits, reserving that type's top value as the exclusion sentinel.
19
+ # A cell that joins no category — a masked cell — is both masked and
20
+ # holds the sentinel, exactly as {CACategorical}'s storage is, so a
21
+ # consumer may read either.
22
+ #
23
+ # Distinctness is the family's hash-key judgement (see {#unique}):
24
+ # `==` for numeric with all NaN collapsed to one value and
25
+ # -0.0 / +0.0 the same value; `eql?` / `hash` for `CA_OBJECT` and
26
+ # `CA_FIXLEN`. Complex is not a lane the factorizer takes and raises
27
+ # {CArray::DataTypeError}.
28
+ #
29
+ # There is no `sort:` here, unlike {#unique} and {#value_counts}:
30
+ # the codes index the levels, so reordering the vocabulary would
31
+ # desync them. Take {#unique}`(sort: true)` where the codes are not
32
+ # wanted, or sort afterwards and carry the codes through the same
33
+ # permutation.
34
+ #
35
+ # @return [Array(CArray, CArray)] `[codes, levels]`.
36
+ def factorize
37
+ # One linear pass through the shared value hash (C
38
+ # __factorize_appearance__). It writes the sentinel into the
39
+ # excluded cells but leaves the codes unmasked; masking them is what
40
+ # CACategorical.from_codes does on its way to the Face, and this
41
+ # surface owes the same, since it hands the storage out bare.
42
+ codes, levels = __factorize_appearance__
43
+ excluded = codes.eq(CACategorical::SENTINEL[codes.data_type])
44
+ if excluded.count(true) > 0
45
+ codes.mask = codes.has_mask? ? (codes.mask | excluded) : excluded
46
+ end
47
+ [codes, levels]
48
+ end
49
+
50
+ end