carray 3.0.0 → 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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +845 -3
  4. data/{NEWS.md → CHANGELOG.v1.md} +1 -1
  5. data/README.md +33 -33
  6. data/carray.gemspec +12 -8
  7. data/ext/ca_axis_dispatch.c +33 -4
  8. data/ext/ca_axis_group.c +256 -114
  9. data/ext/ca_binop_dispatch.c +5 -6
  10. data/ext/ca_binop_dispatch.h +0 -7
  11. data/ext/ca_categorical_iterator.c +121 -67
  12. data/ext/ca_for_each_element.h +6 -8
  13. data/ext/ca_group_iter.c +1 -1
  14. data/ext/ca_kernel_iterator.c +386 -74
  15. data/ext/ca_kernel_iterator.h +145 -45
  16. data/ext/ca_obj_array.c +62 -20
  17. data/ext/ca_obj_bincmp.c +57 -27
  18. data/ext/ca_obj_binop.c +86 -30
  19. data/ext/ca_obj_block.c +4 -4
  20. data/ext/ca_obj_const_string.c +88 -27
  21. data/ext/ca_obj_face.c +104 -0
  22. data/ext/ca_obj_face.h +48 -1
  23. data/ext/ca_obj_fake.c +11 -0
  24. data/ext/ca_obj_fixlen_string.c +21 -6
  25. data/ext/ca_obj_grid.c +11 -3
  26. data/ext/ca_obj_meld.c +125 -22
  27. data/ext/ca_obj_moncmp.c +33 -14
  28. data/ext/ca_obj_monop.c +19 -7
  29. data/ext/ca_obj_object.c +107 -4
  30. data/ext/ca_obj_record.c +3 -1
  31. data/ext/ca_obj_refer.c +30 -6
  32. data/ext/ca_obj_roll.c +13 -5
  33. data/ext/ca_obj_select.c +49 -34
  34. data/ext/ca_obj_select_axis.c +16 -6
  35. data/ext/ca_obj_shift.c +3 -3
  36. data/ext/ca_obj_stack.c +3 -8
  37. data/ext/ca_obj_stride.c +178 -24
  38. data/ext/ca_obj_string.c +11 -5
  39. data/ext/ca_obj_tile.c +13 -5
  40. data/ext/ca_obj_time.c +3 -1
  41. data/ext/ca_obj_timedelta.c +3 -1
  42. data/ext/ca_obj_transpose.c +1 -1
  43. data/ext/ca_obj_triop.c +62 -23
  44. data/ext/ca_obj_window.c +94 -19
  45. data/ext/ca_op_cmplx64.h +123 -0
  46. data/ext/ca_op_ipower.c +1 -5
  47. data/ext/ca_rng_normal.h +42 -0
  48. data/ext/ca_rng_xoshiro256pp.h +105 -0
  49. data/ext/ca_sort_kernels.h +5 -5
  50. data/ext/ca_sweep_engine.c +362 -155
  51. data/ext/ca_sweep_engine.h +39 -9
  52. data/ext/ca_transform_common.c +7 -0
  53. data/ext/carray.h +148 -132
  54. data/ext/carray_access.c +83 -49
  55. data/ext/carray_address_basis.c +590 -0
  56. data/ext/carray_bincount.c +8 -8
  57. data/ext/carray_broadcast.c +102 -9
  58. data/ext/carray_build_flags.h +3 -0
  59. data/ext/carray_call_cfunc.c +2010 -335
  60. data/ext/carray_call_cfunc.h +153 -0
  61. data/ext/carray_cast.c +184 -72
  62. data/ext/carray_conversion.c +22 -22
  63. data/ext/carray_copy.c +67 -32
  64. data/ext/carray_core.c +140 -13
  65. data/ext/carray_count.c +10 -11
  66. data/ext/carray_element.c +7 -7
  67. data/ext/carray_factorize.c +72 -51
  68. data/ext/carray_hold.c +1 -1
  69. data/ext/carray_index_classifier.c +7 -21
  70. data/ext/carray_internal.h +36 -3
  71. data/ext/carray_kernels_bincmp.c +1 -0
  72. data/ext/carray_kernels_binop.c +320 -298
  73. data/ext/carray_kernels_init.c +1095 -0
  74. data/ext/carray_kernels_map.c +1 -0
  75. data/ext/carray_kernels_moncmp.c +1 -0
  76. data/ext/carray_kernels_monop.c +412 -411
  77. data/ext/carray_kernels_reduce_aggregate.c +256 -87
  78. data/ext/carray_kernels_reduce_boolean.c +1 -0
  79. data/ext/carray_kernels_reduce_cumulative.c +324 -54
  80. data/ext/carray_kernels_reduce_extreme.c +605 -58
  81. data/ext/carray_kernels_reduce_variance.c +1 -0
  82. data/ext/carray_kernels_scan.c +5 -4
  83. data/ext/carray_kernels_search.c +187 -106
  84. data/ext/carray_kernels_sort.c +1 -0
  85. data/ext/carray_kernels_triop.c +1 -0
  86. data/ext/carray_lazy.c +175 -5
  87. data/ext/carray_loop.c +7 -1
  88. data/ext/carray_mask.c +29 -13
  89. data/ext/carray_median_percentile.c +56 -1
  90. data/ext/carray_memory_view.c +46 -21
  91. data/ext/carray_operator.c +50 -53
  92. data/ext/carray_order.c +1 -1
  93. data/ext/carray_partition.c +4 -4
  94. data/ext/carray_random.c +391 -47
  95. data/ext/carray_scatter.c +1 -1
  96. data/ext/carray_slab.c +13 -0
  97. data/ext/carray_sort.c +22 -24
  98. data/ext/carray_sort_kernel.c +10 -10
  99. data/ext/carray_test.c +1 -1
  100. data/ext/extconf.rb +18 -0
  101. data/ext/mk_call_cfunc.rb +323 -145
  102. data/ext/mkkernel.rb +651 -129
  103. data/ext/ruby_carray.c +28 -17
  104. data/ext/version.h +4 -4
  105. data/lib/carray/autoload_carray.rb +24 -3
  106. data/lib/carray/autoload_method_extension.rb +14 -1
  107. data/lib/carray/axis_group.rb +84 -7
  108. data/lib/carray/basics.rb +63 -59
  109. data/lib/carray/bincount_nd.rb +12 -12
  110. data/lib/carray/block_iterator.rb +111 -25
  111. data/lib/carray/categorical.rb +154 -37
  112. data/lib/carray/categorical_iterator.rb +300 -151
  113. data/lib/carray/conditional.rb +14 -14
  114. data/lib/carray/const_string.rb +131 -27
  115. data/lib/carray/construct.rb +61 -0
  116. data/lib/carray/core_extensions.rb +62 -5
  117. data/lib/carray/data_type_extension.rb +21 -1
  118. data/lib/carray/data_type_limits.rb +91 -0
  119. data/lib/carray/fixlen_string.rb +1 -1
  120. data/lib/carray/frame/concat.rb +3 -3
  121. data/lib/carray/frame/convert.rb +1 -1
  122. data/lib/carray/frame/csv_parser.rb +11 -4
  123. data/lib/carray/frame/frame.rb +85 -21
  124. data/lib/carray/frame/group.rb +36 -3
  125. data/lib/carray/frame/io.rb +224 -15
  126. data/lib/carray/frame/records.rb +18 -4
  127. data/lib/carray/frame/sort.rb +1 -1
  128. data/lib/carray/frame/verbs.rb +32 -12
  129. data/lib/carray/fuse_source.rb +123 -0
  130. data/lib/carray/fusion.rb +218 -0
  131. data/lib/carray/histogram.rb +16 -16
  132. data/lib/carray/inspect.rb +43 -16
  133. data/lib/carray/iterator.rb +147 -3
  134. data/lib/carray/lazy.rb +88 -73
  135. data/lib/carray/mask_gap_fill.rb +3 -1
  136. data/lib/carray/meld_reduce.rb +2 -2
  137. data/lib/carray/methods/align_addr.rb +1 -1
  138. data/lib/carray/methods/composition.rb +1 -1
  139. data/lib/carray/methods/discovery_along.rb +74 -0
  140. data/lib/carray/methods/factorize.rb +50 -0
  141. data/lib/carray/methods/is_in.rb +25 -14
  142. data/lib/carray/methods/locate_addr.rb +81 -3
  143. data/lib/carray/methods/mask_duplicates.rb +36 -2
  144. data/lib/carray/methods/meshgrid.rb +4 -5
  145. data/lib/carray/methods/mode.rb +2 -2
  146. data/lib/carray/methods/nunique.rb +23 -2
  147. data/lib/carray/methods/repeat.rb +110 -0
  148. data/lib/carray/methods/snap.rb +7 -2
  149. data/lib/carray/methods/unique.rb +44 -4
  150. data/lib/carray/methods/value_counts.rb +2 -2
  151. data/lib/carray/rng.rb +86 -0
  152. data/lib/carray/runtime.rb +0 -19
  153. data/lib/carray/slab_iterator.rb +77 -19
  154. data/lib/carray/string_operation_extension.rb +10 -6
  155. data/lib/carray/time.rb +906 -491
  156. data/lib/carray/window_iterator.rb +421 -27
  157. data/lib/carray.rb +7 -5
  158. data/yard-stubs/ca_obj_array.rb +385 -0
  159. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  160. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  161. data/yard-stubs/ca_obj_block.rb +68 -0
  162. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  163. data/yard-stubs/ca_obj_fake.rb +31 -0
  164. data/yard-stubs/ca_obj_farray.rb +32 -0
  165. data/yard-stubs/ca_obj_field.rb +45 -0
  166. data/yard-stubs/ca_obj_grid.rb +35 -0
  167. data/yard-stubs/ca_obj_refer.rb +72 -0
  168. data/yard-stubs/ca_obj_roll.rb +45 -0
  169. data/yard-stubs/ca_obj_shift.rb +43 -0
  170. data/yard-stubs/ca_obj_stride.rb +181 -0
  171. data/yard-stubs/ca_obj_tile.rb +29 -0
  172. data/yard-stubs/ca_obj_transpose.rb +40 -0
  173. data/yard-stubs/ca_obj_window.rb +57 -0
  174. data/yard-stubs/carray_access.rb +131 -0
  175. data/yard-stubs/carray_attribute.rb +246 -0
  176. data/yard-stubs/carray_broadcast.rb +37 -0
  177. data/yard-stubs/carray_cast.rb +489 -0
  178. data/yard-stubs/carray_class.rb +65 -0
  179. data/yard-stubs/carray_conversion.rb +76 -0
  180. data/yard-stubs/carray_copy.rb +79 -0
  181. data/yard-stubs/carray_core.rb +34 -0
  182. data/yard-stubs/carray_count.rb +84 -0
  183. data/yard-stubs/carray_element.rb +108 -0
  184. data/yard-stubs/carray_generate.rb +66 -0
  185. data/yard-stubs/carray_lazy.rb +228 -0
  186. data/yard-stubs/carray_loop.rb +140 -0
  187. data/yard-stubs/carray_mask.rb +259 -0
  188. data/yard-stubs/carray_math.rb +1615 -0
  189. data/yard-stubs/carray_mathfunc.rb +45 -0
  190. data/yard-stubs/carray_median_percentile.rb +103 -0
  191. data/yard-stubs/carray_memory_view.rb +163 -0
  192. data/yard-stubs/carray_order.rb +252 -0
  193. data/yard-stubs/carray_random.rb +89 -0
  194. data/yard-stubs/carray_scatter.rb +106 -0
  195. data/yard-stubs/carray_slab.rb +59 -0
  196. data/yard-stubs/carray_sort.rb +165 -0
  197. data/yard-stubs/carray_test.rb +85 -0
  198. data/yard-stubs/carray_undef.rb +64 -0
  199. data/yard-stubs/carray_utils.rb +97 -0
  200. data/yard-stubs/ruby_carray.rb +193 -0
  201. metadata +67 -10
  202. data/ext/ca_obj_unbound_repeat.c +0 -496
data/lib/carray/lazy.rb CHANGED
@@ -73,11 +73,11 @@ class CArray
73
73
  rad_2pi: CAMonOp::OP_RAD_2PI,
74
74
  rad_pi: CAMonOp::OP_RAD_PI,
75
75
 
76
- # Sign function (preserve dtype). bool/uint → 0/1, sint → -1/0/1,
76
+ # Sign function (preserves the data type). bool/uint → 0/1, sint → -1/0/1,
77
77
  # float → -1/0/1 NaN-preserving, complex → unit vector or 0.
78
78
  sign: CAMonOp::OP_SIGN,
79
79
 
80
- # imag_i: preserve-dtype primitive (0 for numeric, cimag for complex
80
+ # imag_i: type-preserving primitive (0 for numeric, cimag for complex
81
81
  # in the real slot). Primarily consumed by the `imag` special case
82
82
  # below but also directly callable via `a.lazy.imag_i`; entry here
83
83
  # so the direct call fuses instead of falling to eager.
@@ -118,24 +118,28 @@ class CArray
118
118
  # complex-specific dispatch (= magnitude as float). Re-express that
119
119
  # dispatch via chain composition over existing lazy ops so abs rides
120
120
  # the lazy substrate fully:
121
- # numeric parent: CAMonOp(OP_ABS_I) (1 node)
122
- # complex parent: CAMonOp(cast_f64) ∘ CAMonOp(OP_ABS_I) (2 nodes)
123
- # The complex chain works because ca_monop_abs_i for cmplx128 stores
124
- # |z| in the real slot with imag=0, then cast cmplx128->f64 picks up
125
- # the real part — semantically identical to rb_ca_abs (= abs_i + .real
126
- # + copy). No new C kernel, no new op_id required.
121
+ # numeric parent: CAMonOp(OP_ABS_I) (1 node)
122
+ # complex parent: CAMonOp(cast_<float>) ∘ CAMonOp(OP_ABS_I) (2 nodes)
123
+ # The complex chain works because ca_monop_abs_i stores |z| in the
124
+ # real slot with imag=0, then the cast picks up the real part —
125
+ # semantically identical to the eager abs. The cast target is the
126
+ # real component width of the operand (cmplx64 -> f32, cmplx128 ->
127
+ # f64), matching the eager kernel's `complex: :real_of_source`.
128
+ # No new C kernel, no new op_id required.
127
129
  # ---------------------------------------------------------------------------
128
130
 
131
+ # @private
129
132
  alias_method :__abs_eager__, :abs
130
133
  __abs_eager_um__ = instance_method(:__abs_eager__)
131
134
  define_method(:abs) do
132
135
  if __lazy_view__?
133
136
  abs_i_node = CAMonOp.__build__(self, CAMonOp::OP_ABS_I)
134
137
  if complex?
135
- # CA_FLOAT64 is a Symbol; convert via data_type_code so the cast
138
+ # float_dt is a Symbol; convert via data_type_code so the cast
136
139
  # op_id can be computed by Integer arithmetic.
140
+ float_dt = (data_type == CA_CMPLX64) ? CA_FLOAT32 : CA_FLOAT64
137
141
  CAMonOp.__build__(abs_i_node,
138
- CAMonOp::CAST_BASE + CArray.data_type_code(CA_FLOAT64))
142
+ CAMonOp::CAST_BASE + CArray.data_type_code(float_dt))
139
143
  else
140
144
  abs_i_node
141
145
  end
@@ -174,6 +178,7 @@ class CArray
174
178
  # mutability on the lazy path is not a regression.
175
179
  # ---------------------------------------------------------------------------
176
180
 
181
+ # @private
177
182
  alias_method :__real_eager__, :real
178
183
  __real_eager_um__ = instance_method(:__real_eager__)
179
184
  define_method(:real) do
@@ -216,11 +221,11 @@ class CArray
216
221
  end
217
222
 
218
223
  # ---------------------------------------------------------------------------
219
- # arg lazy fuse: eager `arg` always returns f64 regardless of input
220
- # dtype (data_type-changing monop; see MkKernel.monop :arg output rule).
221
- # CAMonOp's cast-before invariant requires input dtype == output dtype
224
+ # arg lazy fuse: eager `arg` is a data_type-changing monop (see
225
+ # MkKernel.monop :arg output rule).
226
+ # CAMonOp's cast-before invariant requires input data type == output data type
222
227
  # at each in-place step, so `arg` can't sit directly in the substrate.
223
- # Chain compose via the preserve-dtype `arg_i` primitive:
228
+ # Chain compose via the type-preserving `arg_i` primitive:
224
229
  #
225
230
  # integer / bool parent → cast_f64 ∘ arg_i (result f64)
226
231
  # float parent → arg_i (result same float)
@@ -228,14 +233,13 @@ class CArray
228
233
  # component of complex slot; cast extracts
229
234
  # the real part, same trick as abs / imag)
230
235
  #
231
- # Note: eager `arg` always widens to f64. The lazy path preserves
232
- # the operand's float / complex width (f32 → f32, cmplx64 → f32)
233
- # rather than always going to f64 — matches the general lazy-substrate
234
- # rule "scalar keeps operand's precision" (see ca_lazy_wrap_scalar
235
- # header). A user needing exact eager-parity can wrap with
236
- # `.to_type(:float64)` before / after.
236
+ # Both paths keep the operand's float / complex width (f32 → f32,
237
+ # cmplx64 → f32), and both send integers to f64 since pi does not fit
238
+ # an integer slot. That agrees with the general lazy-substrate rule
239
+ # "scalar keeps operand's precision" (see ca_lazy_wrap_scalar header).
237
240
  # ---------------------------------------------------------------------------
238
241
 
242
+ # @private
239
243
  alias_method :__arg_eager__, :arg
240
244
  __arg_eager_um__ = instance_method(:__arg_eager__)
241
245
  define_method(:arg) do
@@ -307,7 +311,6 @@ class CArray
307
311
 
308
312
  # IEEE 754 remainder (distinct semantics from `%` / `mod`: float
309
313
  # branch uses C99 `remainder`, round-half-to-even).
310
- :reminder => CABinOp::OP_REMINDER,
311
314
  }.freeze
312
315
 
313
316
  LAZY_BINOP_OP_IDS.each do |method_name, op_id|
@@ -533,7 +536,7 @@ class CArray
533
536
  if __lazy_view__?
534
537
  dt_name = data_type_name
535
538
  if CScalar.respond_to?(dt_name)
536
- s = CScalar.send(dt_name)
539
+ s = CScalar.public_send(dt_name)
537
540
  s[0] = other
538
541
  [s, self]
539
542
  else
@@ -801,6 +804,15 @@ end
801
804
  end
802
805
  copy
803
806
  end
807
+
808
+ # Where something is registered to compute an expression -- a compiler,
809
+ # say -- it is asked here, and the walk below is what happens when it
810
+ # declines, when nothing is registered, or when the array is small
811
+ # enough that walking is the faster answer.
812
+ def copy
813
+ return super unless CArray.expression_evaluator
814
+ CArray::Fusion.evaluate(self) || super
815
+ end
804
816
  end
805
817
  end
806
818
 
@@ -825,65 +837,68 @@ end
825
837
  # - A bare lazy return auto-materialises; escaping the lazy view (e.g.
826
838
  # stashing it in an Array) is the user's responsibility.
827
839
  # ---------------------------------------------------------------------------
840
+ # CArray.fuse
841
+ #
842
+ # Builds an expression instead of evaluating it, so that it is computed in
843
+ # one pass with no array standing for a step along the way. The block is
844
+ # read rather than called; see CArray::FuseSource for why, and for what
845
+ # happens where its source cannot be read.
846
+ # ---------------------------------------------------------------------------
828
847
  class << CArray
829
- # @overload fuse(*args) { |*shadows| ... }
830
- # Runs a transient lazy-fusion scope: wraps each CArray argument
831
- # with `.lazy`, yields the wrappers (and any non-CArray args)
832
- # to the block, then auto-materialises a bare lazy return value
833
- # into an entity. Non-lazy returns pass through as-is.
834
- # @param args [Array<CArray, Object>] operands.
835
- # @yieldparam shadows [Array<CArray, Object>] lazy wrappers
836
- # paired with pass-through non-CArray operands.
837
- # @return [Object]
838
- # @raise [LocalJumpError] when no block is given.
839
- def fuse(*args)
840
- raise LocalJumpError, "CArray.fuse requires a block" unless block_given?
841
- shadows = args.map { |a| a.is_a?(CArray) ? a.lazy : a }
842
- result = yield(*shadows)
843
- case result
844
- when CAMonOp, CABinOp, CAMonCmp, CABinCmp, CALazyMarker
845
- result.to_ca
846
- else
847
- result
848
+ # The object asked to compute an expression, or nil. CArray can always
849
+ # walk one, so nothing has to be registered and nothing changes when
850
+ # nothing is; what a registered evaluator adds is a second way to arrive
851
+ # at the same answer, and it may decline.
852
+ #
853
+ # It is called as `call(plan, out)` -- see {CArray::Fusion} for what a
854
+ # plan holds -- and answers by filling `out` and returning something
855
+ # true, or by returning something false having written nothing. Raising
856
+ # is not an answer: one that raises is dropped, with a warning, and
857
+ # expressions are walked from there on.
858
+ #
859
+ # @return [#call, nil]
860
+ attr_reader :expression_evaluator
861
+
862
+ # @param object [#call, nil] see {#expression_evaluator}
863
+ # @raise [ArgumentError] when it cannot be called.
864
+ def expression_evaluator= (object)
865
+ if object && ! object.respond_to?(:call)
866
+ raise ArgumentError,
867
+ "an expression evaluator must respond to #call(plan, out)"
848
868
  end
869
+ @expression_evaluator = object
849
870
  end
850
871
 
851
- # CArray.lazy(*args) { |lazies| ... } — dual of fuse
872
+ # @overload fuse { <expression> }
873
+ # Builds the expression rather than evaluating it, so that it is
874
+ # computed in one pass with no array standing for a step along the way.
852
875
  #
853
- # Like `fuse`, wraps each CArray argument with `.lazy` and yields it to
854
- # the block, but **does not auto-materialise at block exit** (= returns
855
- # the lazy structure as-is). If the block return is non-lazy (= Numeric
856
- # / entity CArray / Array etc.) it's pass-through (= same polymorphic
857
- # semantics as fuse).
876
+ # The block is not called. Its source is read, every name in it that
877
+ # holds a CArray is given `.lazy`, and the result is evaluated back in
878
+ # the block's own binding -- so `self`, instance variables, methods and
879
+ # constants are what they were where it was written.
858
880
  #
859
- # Use cases:
860
- # - Passing a chain between functions: build the lazy expression
861
- # inside the function and materialise at the caller
862
- # (= `.to_ca` / `.sum` / `.mean(axis:)` etc.)
863
- # - Reusable expressions: apply the same expr to multiple datasets
864
- # - debug / dump_tree: observe the lazy structure as-is
865
- # - Pick the materialise form later: full materialise or reduction
881
+ # out[] = CArray.fuse { (a + b) * (c - a) }
882
+ # total = CArray.fuse { a * weight }.sum
866
883
  #
867
- # Example:
868
- # expr = CArray.lazy(a, b) { |s, o| (s + o) * 2 }
869
- # expr.class #=> CABinOp (lazy view)
870
- # expr.to_ca # full materialise
871
- # expr.sum # reduction (= chain + reduce in 1 pass)
884
+ # What comes back is the expression, not an array: it is computed where
885
+ # it is used -- stored into an array, reduced, or asked for one with
886
+ # `to_ca`. A block holding anything but an expression over arrays comes
887
+ # back as whatever it evaluated to.
872
888
  #
873
- # Polymorphic semantics (= symmetric with fuse):
874
- # CArray.lazy(25.0, b) { |s, o| s + o } # s=25.0 Float pass-through
875
- # CArray.lazy(arr, b) { |s, o| s + o } # s=arr.lazy
876
- # @overload lazy(*args) { |*shadows| ... }
877
- # Like {.fuse} but does not auto-materialise: returns whatever
878
- # the block yields (typically a lazy view) so the expression
879
- # can be materialised later via `.to_ca`, `.sum`, `.mean`, etc.
880
- # @param args [Array<CArray, Object>] operands.
881
- # @yieldparam shadows [Array<CArray, Object>] lazy wrappers.
882
- # @return [Object]
889
+ # @return [Object] the expression, as a lazy view
883
890
  # @raise [LocalJumpError] when no block is given.
884
- def lazy(*args)
885
- raise LocalJumpError, "CArray.lazy requires a block" unless block_given?
886
- shadows = args.map { |a| a.is_a?(CArray) ? a.lazy : a }
887
- yield(*shadows)
891
+ # @raise [ArgumentError] when the block's source cannot be read -- in
892
+ # irb, in `eval`, or from a file that is gone. Write `.lazy` on the
893
+ # operands there: `a.lazy + b.lazy`.
894
+ def fuse (*args, &block)
895
+ raise LocalJumpError, "CArray.fuse requires a block" unless block
896
+ unless args.empty?
897
+ raise ArgumentError,
898
+ "CArray.fuse takes no arguments: write the expression itself, " \
899
+ "`CArray.fuse { a + b }`, and the names in it are read from " \
900
+ "where the block was written"
901
+ end
902
+ CArray::FuseSource.evaluate(block)
888
903
  end
889
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)
@@ -190,7 +190,7 @@ class CAMeld
190
190
  # is unchanged. (Reduced axis == meld_axis is handled by the callers'
191
191
  # meld_axis branch and never reaches here.)
192
192
  def non_meld_axis_decompose(op, axis)
193
- parts = parents.map { |p| p.send(op, axis: axis) }
193
+ parts = parents.map { |p| p.public_send(op, axis: axis) }
194
194
  axis_norm = axis < 0 ? axis + ndim : axis
195
195
  new_meld_axis = axis_norm < meld_axis ? meld_axis - 1 : meld_axis
196
196
  CArray.meld(parts, axis: new_meld_axis).copy
@@ -202,7 +202,7 @@ class CAMeld
202
202
  # Empty-parent / short-parent handling — Welford needs n >= 2 per parent
203
203
  # to recover m2 from p.variance for sample, n >= 1 for variancep (n=1
204
204
  # gives m2=0, fine). Any parent below its threshold punts to super,
205
- # which handles UNDEF / 0-count per CLAUDE.md contract.
205
+ # which handles the UNDEF / zero-count cases.
206
206
  min_n = sample ? 2 : 1
207
207
  if axis.nil?
208
208
  return yield if parents.any? { |p| p.elements < min_n }
@@ -33,7 +33,7 @@ class CArray
33
33
  #
34
34
  # Value equality follows the value-hash discovery family (numeric `==` with
35
35
  # NaN collapsed and `-0.0 == +0.0`; object `hash` / `eql?`; fixlen byte
36
- # equality). Arrays are coerced to the first array's dtype within the same
36
+ # equality). Arrays are coerced to the first array's data type within the same
37
37
  # family (cross-family raises). Masked cells do not enter `common`.
38
38
  #
39
39
  # @param arrays [Array<CArray>] two or more arrays (Array / Range coerced
@@ -12,7 +12,7 @@
12
12
  # (`stack` / `meld` / `montage`, plus `CArray#stack` and `CAStack#append`)
13
13
  # lives in carray/stack.rb. The cheat sheet below covers the whole family.
14
14
  #
15
- # | view (same dtype) | eager (auto-cast)
15
+ # | view (same data type) | eager (auto-cast)
16
16
  # -------------------+------------------------+-----------------------
17
17
  # concat existing | meld (CAMeld view) | concatenate (materialised)
18
18
  # axis (ndim same) | |
@@ -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
@@ -10,19 +10,22 @@ class CArray
10
10
  # argument is accepted; to test a few immediate values pass an
11
11
  # Array (`a.is_in([0, -1])`).
12
12
  #
13
- # When `self` and `values` have different numeric dtypes they are
13
+ # When `self` and `values` have different numeric data types they are
14
14
  # promoted to a common type first (the same promotion binops use,
15
15
  # {CArray.result_type}), so membership is value-correct across
16
- # dtypes (e.g. an int cell equals a float set element of the same
16
+ # data types (e.g. an int cell equals a float set element of the same
17
17
  # value, and a fractional set element never truncates onto an int
18
- # cell). Genuinely incompatible dtypes (e.g. numeric vs fixlen)
18
+ # cell). Genuinely incompatible data types (e.g. numeric vs fixlen)
19
19
  # raise.
20
20
  #
21
21
  # Membership is value-based and shares the distinctness of the
22
22
  # value-hash discovery family ({#unique} / {#value_counts}):
23
23
  # numeric follows `==` with all NaN collapsed to one value and
24
24
  # -0.0 == +0.0; `CA_OBJECT` follows Ruby `hash` / `eql?` with Float
25
- # NaN collapsed; `CA_FIXLEN` follows byte equality.
25
+ # NaN collapsed; `CA_FIXLEN` follows byte equality over the whole
26
+ # cell, with a short String in the set padded out to the cell width
27
+ # (so a 5-byte array written from `"be"` is `is_in(["be"])`). A
28
+ # `CA_FIXLEN` set given as a CArray must already be of that width.
26
29
  #
27
30
  # Masked cells of `values` do not enter the set. Masked cells of
28
31
  # `self` stay masked in the result (membership is unknown), so
@@ -32,7 +35,7 @@ class CArray
32
35
  # reduction, compose with {#any}: `a.is_in(values).any(axis: k)`.
33
36
  #
34
37
  # @param values [CArray, Array, Range] the set to test membership
35
- # against. Promoted with `self` to a common dtype.
38
+ # against. Promoted with `self` to a common data type.
36
39
  # Between two time arrays the question is about instants, not ticks:
37
40
  # `values` is reconciled into `self`'s unit first, so a `:D` array and an
38
41
  # `:h` array match on the instants they share. The same holds for the set
@@ -49,7 +52,7 @@ class CArray
49
52
  # `self` and `other`, in `self`'s first-appearance order.
50
53
  #
51
54
  # Value-based, sharing the distinctness of the discovery family
52
- # (see {#is_in}); `self` and `other` are promoted to a common dtype.
55
+ # (see {#is_in}); `self` and `other` are promoted to a common data type.
53
56
  # Masked cells of either array do not participate. The result is
54
57
  # always flat, like {#unique}, because the distinct values of a
55
58
  # fiber vary in number.
@@ -97,18 +100,18 @@ class CArray
97
100
 
98
101
  # Reconcile a set-valued argument with self to a common data_type via
99
102
  # CArray.result_type (the single-source promotion rule the eager binop and
100
- # lazy CABinOp share), returning [self', set']. Only dtypes are reconciled,
103
+ # lazy CABinOp share), returning [self', set']. Only data types are reconciled,
101
104
  # never shapes: unlike the binop coercion (cast_self_or_other) the set's
102
105
  # shape never broadcasts against self's, so a size-1 self keeps its shape.
103
- # result_type raises for genuinely incompatible dtypes (numeric vs fixlen).
106
+ # result_type raises for genuinely incompatible data types (numeric vs fixlen).
104
107
  #
105
- # A bare Array / Range has no intrinsic dtype, so the common type is inferred
108
+ # A bare Array / Range has no intrinsic data type, so the common type is inferred
106
109
  # from self and the individual elements (result_type classifies each scalar).
107
110
  # This keeps a fractional literal from truncating onto an int self, and an
108
111
  # int literal from boxing a float self into the object lane (where Float 2.0
109
- # is not eql? Integer 2). A CArray argument uses its own dtype; any other
112
+ # is not eql? Integer 2). A CArray argument uses its own data type; any other
110
113
  # operand (Numo, a MemoryView producer, ...) comes in through wrap_readonly,
111
- # the canonical type-coercion entry, so its format's dtype drives the promote.
114
+ # the canonical type-coercion entry, so its format's data type drives the promote.
112
115
  def promote_value_set (values)
113
116
  case values
114
117
  when Array then return promote_elements(values)
@@ -123,10 +126,18 @@ class CArray
123
126
  # Bare Array / Range against numeric self: infer the common numeric type
124
127
  # from self and the elements (each classified by result_type), so a
125
128
  # fractional literal promotes self to float instead of truncating. Against
126
- # object / fixlen self the elements are values, not dtype specifiers (a
127
- # String is a value, not a type name), so build the set in self's dtype.
129
+ # object / fixlen self the elements are values, not data type specifiers (a
130
+ # String is a value, not a type name), so build the set in self's data type.
128
131
  def promote_elements (elems)
129
- if data_type == CA_OBJECT || data_type == CA_FIXLEN
132
+ if data_type == CA_FIXLEN
133
+ # A fixlen value is a cell-width blob, so the set has to be built at
134
+ # this array's width -- to_type(:fixlen) with no bytes: builds it at
135
+ # width 0, where every element is "" and the C guard then refuses the
136
+ # set for not matching self. Same rule the scalar operand of a
137
+ # comparison follows: a String standing in for a cell is padded to the
138
+ # cell's width.
139
+ [self, elems.to_ca.to_type(data_type, bytes: bytes)]
140
+ elsif data_type == CA_OBJECT
130
141
  [self, elems.to_ca.to_type(data_type)]
131
142
  else
132
143
  t = CArray.result_type(self, *elems)
@@ -1,8 +1,47 @@
1
1
  class CArray
2
2
 
3
- # User-facing YARD docs for #locate_addr and #locate_nearest_addr live
4
- # in yard-stubs/carray_order.rb (grouped with the search family).
3
+ # @!group Sorting and searching
5
4
 
5
+ # @overload locate_addr(ref)
6
+ # Returns, for each element of `self`, the flat address into `ref`
7
+ # where the value first occurs, or `UNDEF` where it is not present.
8
+ # Builds a value-to-first-address map from `ref` in one pass (an
9
+ # open-addressing hash, the same substrate as {#unique} /
10
+ # {#value_counts}), then probes each element of `self`; no sort,
11
+ # peak memory `O(distinct ref values)`.
12
+ #
13
+ # The return is `ref`'s flat address (0 to `ref.elements - 1`), so a
14
+ # multi-dimensional `ref` still yields a `self`-shaped result of flat
15
+ # addresses; downstream reads (`ref[addr]`, `model_var[addr]`, ...)
16
+ # apply it as a flat gather.
17
+ #
18
+ # Works on numeric, `CA_OBJECT`, and `CA_FIXLEN` values, matching the
19
+ # value-hash discovery family: numeric follows `==` with all NaN
20
+ # collapsed to one value and `-0.0 == +0.0`; object follows Ruby
21
+ # `hash` / `eql?` with Float NaN collapsed; fixlen follows byte
22
+ # equality. When `ref` holds duplicate values the returned address is
23
+ # the earliest (appearance-order) occurrence. Masked cells of `ref` do
24
+ # not enter the map but still occupy their flat address; masked cells
25
+ # of `self` are `UNDEF` in the result.
26
+ #
27
+ # `self` and `ref` are compared at their common type, negotiated by
28
+ # `CArray.result_type` — a fractional query against an integer `ref`
29
+ # is compared at the promoted type rather than truncated, so `1.5` no
30
+ # longer matches `1`. Cross-family input (numeric against fixlen)
31
+ # raises.
32
+ #
33
+ # Typical use is time-axis lookup: compute the address once against a
34
+ # reference axis, then reuse it to gather from many `ref`-shaped
35
+ # variables without repeating the lookup.
36
+ #
37
+ # @param ref [CArray, Array, Range] reference values to match against;
38
+ # any shape (used as flat). An Array or Range is coerced with `to_ca`
39
+ # and so lands in `:object`, where numeric matching follows Ruby
40
+ # `eql?` (`2.0` does not match `2`); pass a CArray to match in a
41
+ # numeric lane.
42
+ # @return [CArray] `:int64` flat addresses into `ref`, same shape as
43
+ # `self`; unmatched cells are masked.
44
+ # @raise [RuntimeError] when `self` and `ref` have no common data type.
6
45
  def locate_addr (ref)
7
46
  ref = ref.to_ca unless ref.is_a?(CArray)
8
47
  # Put self and ref in a common lane via the single-source promotion rule
@@ -16,6 +55,38 @@ class CArray
16
55
  q.send(:__locate_addr__, r)
17
56
  end
18
57
 
58
+ # @overload locate_nearest_addr(ref, direction: :round, tolerance: nil)
59
+ # Returns, for each element of `self`, the flat address into `ref` of
60
+ # the nearest reference value. Continuous sibling of {#locate_addr};
61
+ # uses `linear_section` + rounding for non-exact matching against a
62
+ # sorted `ref`.
63
+ #
64
+ # Out-of-range cells of `self` (outside `ref`'s span) mask through the
65
+ # pipeline: `linear_section` returns NaN, `mask_invalid` propagates
66
+ # that as `UNDEF`, rounding and the int64 cast carry the mask, and
67
+ # `project` scatters it into the final positions. `mask_invalid` runs
68
+ # before rounding because `CArray#round` maps NaN to 0 and would
69
+ # otherwise silently match `ref[0]`.
70
+ #
71
+ # `tolerance:` (default `nil`) sets a maximum accepted absolute
72
+ # distance between `self[i]` and its matched `ref` value. When
73
+ # `|ref[addr] - self[i]| > tolerance`, the result cell is masked. Use
74
+ # for accuracy-controlled matching (e.g. "an observation snaps to a
75
+ # time step only if within N seconds").
76
+ #
77
+ # `ref` need not be given in ascending order: it is sorted internally
78
+ # and the returned addresses are mapped back to positions in `ref` as
79
+ # passed.
80
+ #
81
+ # @param ref [CArray] 1-D reference grid to match against.
82
+ # @param direction [Symbol] `:round`, `:floor`, or `:ceil` — rounding
83
+ # applied to the fractional position.
84
+ # @param tolerance [Numeric, nil] maximum accepted `|self - ref|`
85
+ # distance; cells beyond this are masked. `nil` disables the check.
86
+ # @return [CArray] `:int64` flat addresses into `ref`, same shape as
87
+ # `self`; out-of-range and beyond-tolerance cells are masked.
88
+ # @raise [ArgumentError] when `direction` is not one of the accepted
89
+ # symbols.
19
90
  def locate_nearest_addr (ref, direction: :round, tolerance: nil)
20
91
  unless [:round, :floor, :ceil].include?(direction)
21
92
  raise ArgumentError,
@@ -35,7 +106,12 @@ class CArray
35
106
  fill[] = sec.nil? ? UNDEF : sec
36
107
  sec = fill
37
108
  end
38
- si = sec.mask_invalid.send(direction).int64
109
+ masked = sec.mask_invalid
110
+ si = case direction
111
+ when :round then masked.round
112
+ when :floor then masked.floor
113
+ when :ceil then masked.ceil
114
+ end.int64
39
115
  idx = ri.project(si)
40
116
  if tolerance
41
117
  dist = (ref.project(idx) - self).abs
@@ -44,4 +120,6 @@ class CArray
44
120
  idx
45
121
  end
46
122
 
123
+ # @!endgroup
124
+
47
125
  end