Class: CArray

Inherits:
Object
  • Object
show all
Extended by:
AutoloadMethodExtension, DataTypeExtension
Defined in:
lib/carray.rb,
lib/carray.rb,
lib/carray/lazy.rb,
lib/carray/time.rb,
lib/carray/stack.rb,
lib/carray/stack.rb,
lib/carray/table.rb,
lib/carray/basics.rb,
lib/carray/string.rb,
lib/carray/struct.rb,
lib/carray/struct.rb,
lib/carray/complex.rb,
lib/carray/inspect.rb,
lib/carray/runtime.rb,
lib/carray/attribute.rb,
lib/carray/construct.rb,
lib/carray/construct.rb,
lib/carray/construct.rb,
lib/carray/histogram.rb,
lib/carray/histogram.rb,
lib/carray/serialize.rb,
lib/carray/axis_group.rb,
lib/carray/bincount_nd.rb,
lib/carray/bincount_nd.rb,
lib/carray/categorical.rb,
lib/carray/conditional.rb,
lib/carray/methods/bin.rb,
lib/carray/arrow_tensor.rb,
lib/carray/const_string.rb,
lib/carray/methods/join.rb,
lib/carray/methods/mode.rb,
lib/carray/methods/snap.rb,
lib/carray/fixlen_string.rb,
lib/carray/mask_gap_fill.rb,
lib/carray/methods/index.rb,
lib/carray/methods/is_in.rb,
lib/carray/block_iterator.rb,
lib/carray/boolean_reduce.rb,
lib/carray/methods/choose.rb,
lib/carray/methods/resize.rb,
lib/carray/methods/unique.rb,
lib/carray/autoload_carray.rb,
lib/carray/autoload_carray.rb,
lib/carray/autoload_carray.rb,
lib/carray/autoload_carray.rb,
lib/carray/autoload_carray.rb,
lib/carray/autoload_carray.rb,
lib/carray/autoload_carray.rb,
lib/carray/autoload_carray.rb,
lib/carray/autoload_carray.rb,
lib/carray/autoload_carray.rb,
lib/carray/core_extensions.rb,
lib/carray/methods/nunique.rb,
lib/carray/window_iterator.rb,
lib/carray/methods/bincount.rb,
lib/carray/methods/meshgrid.rb,
lib/carray/methods/broadcast.rb,
lib/carray/methods/gather_nd.rb,
lib/carray/methods/align_addr.rb,
lib/carray/methods/bit_string.rb,
lib/carray/data_type_extension.rb,
lib/carray/methods/composition.rb,
lib/carray/methods/locate_addr.rb,
lib/carray/categorical_iterator.rb,
lib/carray/methods/insert_block.rb,
lib/carray/methods/value_counts.rb,
lib/carray/methods/string_format.rb,
lib/carray/methods/mask_duplicates.rb,
lib/carray/string_operation_extension.rb

Overview

Shared String-operation surface for the String Faces: the CArray::StringOperationMixin (included by CAString / CAFixlenString / CAConstString).

Defined Under Namespace

Modules: ArrowTensor, CoreExtensions, DataTypeExtension, DataTypeNewConstructor, StringOperationMixin, TableMethods Classes: BincountND, Boolean, Complex128, Complex64, Fixlen, Float32, Float64, Histogram, Inspector, Int16, Int32, Int64, Int8, Object, Serializer, UInt16, UInt32, UInt64, UInt8

Constant Summary collapse

LAZY_MONOP_OP_IDS =

op_name => CAMonOp::OP_

{
  # Preserve-data_type monop (8)
  zero:    CAMonOp::OP_ZERO,
  one:     CAMonOp::OP_ONE,
  frac:    CAMonOp::OP_FRAC,
  neg:     CAMonOp::OP_NEG,
  bit_neg: CAMonOp::OP_BIT_NEG,
  abs_i:   CAMonOp::OP_ABS_I,
  conj:    CAMonOp::OP_CONJ,
  not:     CAMonOp::OP_NOT,

  # Preserve-data_type monfunc (4)
  ceil:    CAMonOp::OP_CEIL,
  floor:   CAMonOp::OP_FLOOR,
  round:   CAMonOp::OP_ROUND,
  rcp:     CAMonOp::OP_RCP,

  # Widening monfunc (22)
  rad:     CAMonOp::OP_RAD,
  deg:     CAMonOp::OP_DEG,
  sqrt:    CAMonOp::OP_SQRT,
  exp:     CAMonOp::OP_EXP,
  exp2:    CAMonOp::OP_EXP2,
  exp10:   CAMonOp::OP_EXP10,
  log:     CAMonOp::OP_LOG,
  log10:   CAMonOp::OP_LOG10,
  log2:    CAMonOp::OP_LOG2,
  logb:    CAMonOp::OP_LOGB,
  sin:     CAMonOp::OP_SIN,
  cos:     CAMonOp::OP_COS,
  tan:     CAMonOp::OP_TAN,
  asin:    CAMonOp::OP_ASIN,
  acos:    CAMonOp::OP_ACOS,
  atan:    CAMonOp::OP_ATAN,
  sinh:    CAMonOp::OP_SINH,
  cosh:    CAMonOp::OP_COSH,
  tanh:    CAMonOp::OP_TANH,
  asinh:   CAMonOp::OP_ASINH,
  acosh:   CAMonOp::OP_ACOSH,
  atanh:   CAMonOp::OP_ATANH,

  # Additional monfunc
  expm1:   CAMonOp::OP_EXPM1,
  log1p:   CAMonOp::OP_LOG1P,
  rsqrt:   CAMonOp::OP_RSQRT,
  trunc:   CAMonOp::OP_TRUNC,
  square:  CAMonOp::OP_SQUARE,

  # Angle normalisation
  deg_360: CAMonOp::OP_DEG_360,
  deg_180: CAMonOp::OP_DEG_180,
  rad_2pi: CAMonOp::OP_RAD_2PI,
  rad_pi:  CAMonOp::OP_RAD_PI,

  # Sign function (preserve dtype).  bool/uint → 0/1, sint → -1/0/1,
  # float → -1/0/1 NaN-preserving, complex → unit vector or 0.
  sign:    CAMonOp::OP_SIGN,

  # imag_i: preserve-dtype primitive (0 for numeric, cimag for complex
  # in the real slot).  Primarily consumed by the `imag` special case
  # below but also directly callable via `a.lazy.imag_i`; entry here
  # so the direct call fuses instead of falling to eager.
  imag_i:  CAMonOp::OP_IMAG_I,
}.freeze
LAZY_BINOP_OP_IDS =

Binop dispatch. Operator entries are redefined so a lazy operand on either side routes into CABinOp.build. Op scope:

  • 5 arithmetic: + - * / **
  • 3 bitwise: & | ^
  • 2 shifts: << >>

- 2 misc: %, rcp_mul

{
  :+         => CABinOp::OP_ADD,
  :-         => CABinOp::OP_SUB,
  :*         => CABinOp::OP_MUL,
  :/         => CABinOp::OP_DIV,
  :**        => CABinOp::OP_POW,
  :&         => CABinOp::OP_BIT_AND,
  :|         => CABinOp::OP_BIT_OR,
  :^         => CABinOp::OP_BIT_XOR,
  :<<        => CABinOp::OP_BIT_LSHIFT,
  :>>        => CABinOp::OP_BIT_RSHIFT,
  :%         => CABinOp::OP_MOD,
  :rcp_mul   => CABinOp::OP_RCP_MUL,

  # Float-only binops registered eagerly by mkkernel; the lazy entries
  # here pick them up so `a.lazy.hypot(b)` etc. ride the substrate.
  :copysign  => CABinOp::OP_COPYSIGN,
  :logaddexp => CABinOp::OP_LOGADDEXP,
  :nextafter => CABinOp::OP_NEXTAFTER,
  :fmod      => CABinOp::OP_FMOD,
  :atan2     => CABinOp::OP_ATAN2,
  :hypot     => CABinOp::OP_HYPOT,

  # Pair-wise max / min (NaN-skip via C99 fmax/fmin on float branch).
  :pmax      => CABinOp::OP_PMAX,
  :pmin      => CABinOp::OP_PMIN,

  # Pair-wise max / min, NaN-propagate variant.
  :maximum   => CABinOp::OP_MAXIMUM,
  :minimum   => CABinOp::OP_MINIMUM,

  # Boolean word forms (bool + object; plain mask propagation, no
  # Kleene fixup — see the CA_BINOP_AND note in ca_binop_dispatch.h).
  :and       => CABinOp::OP_AND,
  :or        => CABinOp::OP_OR,
  :xor       => CABinOp::OP_XOR,

  # IEEE 754 remainder (distinct semantics from `%` / `mod`: float
  # branch uses C99 `remainder`, round-half-to-even).
  :reminder  => CABinOp::OP_REMINDER,
}.freeze
LAZY_TRIOP_OP_IDS =

Triop dispatch (fma / fms / clip). CATriOp is the CABinOp analog for three-operand element-wise ops. Each Ruby method redefined below dispatches to CATriOp.build when any of self / op2 / op3 is a lazy view, and falls to the eager C method otherwise.

clip is the strict-clamp entry (__clip_ki__, called by the lib/carray/basics.rb clip wrapper's both-bounds-present path). The nil-bound one-sided cases route through the wrapper's pmax / pmin calls, which themselves lazy-fuse via LAZY_BINOP_OP_IDS above — so a.lazy.clip(nil, hi) and a.lazy.clip(lo, nil) fuse without a

dedicated triop entry.

{
  fma:         CATriOp::OP_FMA,
  fms:         CATriOp::OP_FMS,
  __clip_ki__: CATriOp::OP_CLIP,
}.freeze
LAZY_BINCMP_OP_IDS =

coerce: when self is a lazy view and the scalar appears on the LEFT (e.g. 2 * a.lazy), Ruby's Numeric#* calls a.lazy.coerce(2). The default coerce would unwrap the lazy-ness via eager scalar promotion; here we keep it lazy by returning [scalar_as_cscalar, self], so the subsequent operator call ends up with a lazy receiver and triggers

the CABinOp builder.

bincmp / moncmp dispatch.

Comparison output is always boolean8_t, so it cannot reuse the CABinOp in-place trick. CABinCmp pulls both operands into operand-data_type scratches and writes boolean to the output buffer. Integer is_nan / is_inf / is_finite use existing per-data_type kernels (which handle the integer const-false/true result and mask skip).

Scope: 7 bincmp + 3 moncmp + operator aliases (< / > / <= / >=) for canonical Ruby comparison syntax. Note: == / eql? are NOT comparison ops — eager CArray#== (rb_ca_equal) is array-level equality returning bool, not element-wise. Element-wise equality is eq / feq.

feq is arity 1 in eager (compile-time FLT_EPSILON / DBL_EPSILON); we mirror that here. Runtime eps is a future extension (struct field

already reserved).

{
  # Canonical method names + operator aliases (rb_define_alias in C
  # creates separate dispatch entries, so we override both).
  :lt  => CABinCmp::OP_LT,
  :<   => CABinCmp::OP_LT,
  :gt  => CABinCmp::OP_GT,
  :>   => CABinCmp::OP_GT,
  :le  => CABinCmp::OP_LE,
  :<=  => CABinCmp::OP_LE,
  :ge  => CABinCmp::OP_GE,
  :>=  => CABinCmp::OP_GE,
  :eq  => CABinCmp::OP_EQ,
  :ne  => CABinCmp::OP_NE,
  :feq => CABinCmp::OP_FEQ,
}.freeze
LAZY_BINCMP_TOL_OP_IDS =

tolerance-bearing bincmp ops (= is_close / is_equiv) use the same CABinCmp dispatch but with a 2nd tol positional arg. Kept in a separate dict because the LAZY_BINCMP_OP_IDS define_method block above uses arity 1 (|other|); these need arity 2 (|other, tol|).

{
  :is_close => CABinCmp::OP_IS_CLOSE,
  :is_equiv => CABinCmp::OP_IS_EQUIV,
}.freeze
SFloat =

Numo-compatible alias of Float32.

Float32
DFloat =

Numo-compatible alias of Float64.

Float64
SComplex =

Numo-compatible alias of Complex64.

Complex64
DComplex =

Numo-compatible alias of Complex128.

Complex128
RObject =

Numo-compatible alias of Object.

Object

Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DataTypeExtension

arange, empty, eye, full, identity, linspace, ones, zeros

Class Method Details

._epoch_seconds_exact(spec, format = nil) ⇒ Object

Exact Rational seconds since the Unix epoch for a start literal (Time / DateTime / Integer unix-seconds / String). UTC default.



1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
# File 'lib/carray/time.rb', line 1500

def self._epoch_seconds_exact(spec, format = nil)
  require 'date'
  require 'time'
  case spec
  when Time     then spec.to_r
  when Integer  then Rational(spec)
  when String
    h = format ? Date._strptime(spec, format) : Date._parse(spec)
    unless h && h[:year] && h[:mon] && h[:mday]
      raise ArgumentError, "cannot parse time #{spec.inspect}"
    end
    days = CATime.send(:_days_from_civil,
                             CA_INT64([h[:year]]), CA_INT64([h[:mon]]),
                             CA_INT64([h[:mday]]))[0]
    sec  = Rational(days * 86400 + (h[:hour] || 0) * 3600 +
                    (h[:min] || 0) * 60 + (h[:sec] || 0))
    sec += h[:sec_fraction] if h[:sec_fraction]
    sec -= h[:offset]       if h[:offset]      # east-of-UTC offset -> UTC
    sec
  else
    if defined?(DateTime) && spec.is_a?(DateTime)
      spec.to_time.to_r
    else
      raise ArgumentError, "cannot parse time #{spec.class}"
    end
  end
end

._epoch_tick_index(spec, res, format = nil) ⇒ Object

Tick index of spec's instant on the res grid (floor toward the past).



1551
1552
1553
1554
1555
1556
1557
1558
1559
# File 'lib/carray/time.rb', line 1551

def self._epoch_tick_index(spec, res, format = nil)
  if CATimeUnitAlgebra::FIXED.key?(res.base)
    (_epoch_seconds_exact(spec, format) / res.tick_ratio).floor
  else
    y, m    = _epoch_year_month(spec, format)
    months  = (y - 1970) * 12 + (m - 1)
    (Rational(months) / res.tick_ratio).floor
  end
end

._epoch_year_month(spec, format = nil) ⇒ Object

year, month of a start literal, for a calendar-resolution grid.



1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
# File 'lib/carray/time.rb', line 1529

def self._epoch_year_month(spec, format = nil)
  require 'date'
  require 'time'
  case spec
  when Time    then t = spec.utc; [t.year, t.month]
  when Integer then t = Time.at(spec, in: 'UTC'); [t.year, t.month]
  when String
    h = format ? Date._strptime(spec, format) : Date._parse(spec)
    unless h && h[:year]
      raise ArgumentError, "cannot parse time #{spec.inspect}"
    end
    [h[:year], h[:mon] || 1]
  else
    if defined?(DateTime) && spec.is_a?(DateTime)
      t = spec.to_time.utc; [t.year, t.month]
    else
      raise ArgumentError, "cannot parse time #{spec.class}"
    end
  end
end

._time_cell(literal, res, format, on_error) ⇒ Object

Single-literal build for time: a 1-element CATime, honouring the on_error policy (raise, or a masked cell).



1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
# File 'lib/carray/time.rb', line 1665

def self._time_cell(literal, res, format, on_error)
  raw = CArray.int64(1)
  begin
    raw[0] = _epoch_tick_index(literal, res, format)
  rescue ArgumentError, TypeError
    raise if on_error == :raise
    raw[0] = UNDEF
  end
  raw.time(unit: res)
end

.align_addr(*arrays, join: :outer) ⇒ Array<CArray>

Aligns several arrays onto one common set of coordinate values and returns, for each array, the flat addresses that gather it onto that common set. Symmetric N-ary counterpart of the instance method #locate_addr (which is the asymmetric self-against-ref lookup).

Returns [common, idx_0, idx_1, ...]:

  • common is a 1-D CArray of the common coordinate values, chosen by join: (see below), in first-appearance order.
  • idx_k is a common-shaped :int64 CArray of flat addresses into arrays[k]: idx_k[j] is where common[j] lives in arrays[k], or UNDEF when that array lacks the value. Each idx_k is exactly common.locate_addr(arrays[k]).

Reindex each array onto the common grid with project, then compare element-wise (missing coordinates come through masked):

common, idx_a, idx_b = CArray.align_addr(a_coord, b_coord, join: :outer) a_on_grid = a_data.project(idx_a) # common-shaped, UNDEF where a lacks it b_on_grid = b_data.project(idx_b)

Because the addresses are returned (not the reindexed values), one alignment serves any number of arrays[k]-shaped variables — compute the idx once, project many.

join: selects the common coordinate set:

  • :outer — union of the distinct values of every array.
  • :inner — distinct values present in every array.
  • :left — the first array's distinct values.
  • :right — the last array's distinct values.

Value equality follows the value-hash discovery family (numeric == with NaN collapsed and -0.0 == +0.0; object hash / eql?; fixlen byte equality). Arrays are coerced to the first array's dtype within the same family (cross-family raises). Masked cells do not enter common.

Parameters:

  • arrays (Array<CArray>)

    two or more arrays (Array / Range coerced via to_ca). One array is allowed (degenerate: common is its distinct values).

  • join (:outer, :inner, :left, :right) (defaults to: :outer)

    how to build common.

Returns:

Raises:

  • (ArgumentError)

    when no array is given or join is not one of the accepted symbols.

Raises:

  • (ArgumentError)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/carray/methods/align_addr.rb', line 46

def self.align_addr (*arrays, join: :outer)
  raise ArgumentError, "align_addr: need at least one array" if arrays.empty?
  arrays = arrays.map { |a| a.is_a?(CArray) ? a : a.to_ca }
  # Seed the fold with the first array's distinct values so N == 1 and the
  # union/intersection folds all agree (a bare reduce over one element would
  # return it with duplicates intact).
  seed = arrays.first.unique
  common = case join
           when :outer then arrays[1..-1].reduce(seed) { |acc, a| acc.union(a) }
           when :inner then arrays[1..-1].reduce(seed) { |acc, a| acc.intersection(a) }
           when :left  then seed
           when :right then arrays.last.unique
           else
             raise ArgumentError,
                   "align_addr: join must be :outer / :inner / :left / :right " \
                   "(got #{join.inspect})"
           end
  idxs = arrays.map { |a| common.locate_addr(a) }
  [common, *idxs]
end

.align_nearest_addr(*arrays, grid: nil, direction: :round, tolerance: nil) ⇒ Array<CArray>

Aligns several arrays onto one common coordinate grid by nearest match, the ordered-lane (continuous) sibling of align_addr. Returns [common, idx_0, idx_1, ...] with the same reindex contract: idx_k is a common-shaped :int64 array of flat addresses into arrays[k] giving, for each grid point, the nearest value in that array — exactly common.locate_nearest_addr(arrays[k], direction:, tolerance:).

Unlike align_addr, the common grid is not built by a set union: continuous coordinates rarely coincide exactly, so a union would merely pile up near-duplicate points. Instead the grid is a reference axis — grid: when given, otherwise the first array verbatim (kept as-is, not deduplicated). Pass grid: arrays.last to align onto the last array's axis. (join: has no meaning here and is not accepted; clustering nearby coordinates into a synthesised grid is out of scope.)

common, ia, ib = CArray.align_nearest_addr(a_coord, b_coord, grid: ref) a_on_grid = a_data.project(ia) # each grid point <- nearest a value b_on_grid = b_data.project(ib)

direction: (:round / :floor / :ceil) and tolerance: are forwarded to #locate_nearest_addr: out-of-range grid points, and points whose nearest value is farther than tolerance, come back masked.

Parameters:

  • arrays (Array<CArray>)

    one or more arrays (Array / Range coerced via to_ca).

  • grid (CArray, Array, Range, nil) (defaults to: nil)

    the reference coordinate grid; nil uses the first array verbatim.

  • direction (:round, :floor, :ceil) (defaults to: :round)

    rounding rule for the nearest match.

  • tolerance (Numeric, nil) (defaults to: nil)

    maximum accepted distance; farther grid points are masked. nil disables the check.

Returns:

Raises:

  • (ArgumentError)

    when no array is given (or direction is invalid, raised by #locate_nearest_addr).

Raises:

  • (ArgumentError)


102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/carray/methods/align_addr.rb', line 102

def self.align_nearest_addr (*arrays, grid: nil, direction: :round, tolerance: nil)
  raise ArgumentError, "align_nearest_addr: need at least one array" if arrays.empty?
  arrays = arrays.map { |a| a.is_a?(CArray) ? a : a.to_ca }
  common = if grid.nil?
             arrays.first
           else
             grid.is_a?(CArray) ? grid : grid.to_ca
           end
  idxs = arrays.map { |a|
    common.locate_nearest_addr(a, direction: direction, tolerance: tolerance)
  }
  [common, *idxs]
end

.concatenate(list, axis: 0, data_type: nil) ⇒ CArray

Returns list concatenated along a single existing axis. Eager (returns a fresh CArray) and accepts non-uniform pieces (varying sizes along the axis); non-tile axes must agree across pieces.

Use CArray.meld for the uniform-shape view-default counterpart.

Parameters:

  • list (Array<CArray>)

    pieces to concatenate.

  • axis (Integer) (defaults to: 0)

    axis to concatenate along.

  • data_type (Symbol, Integer, nil) (defaults to: nil)

    result data_type; inferred via result_type when nil.

Returns:

  • (CArray)

    fresh CArray with per-piece axis sizes summed.

Raises:

  • (ArgumentError)

    when list is empty or piece shapes are inconsistent.

Raises:

  • (ArgumentError)


84
85
86
87
# File 'lib/carray/methods/composition.rb', line 84

def self.concatenate (list, axis: 0, data_type: nil)
  raise ArgumentError, "concatenate: list must not be empty" if list.empty?
  __ragged_paste(list, [list.size], axis, data_type)
end

.const_string(values, encoding: Encoding::UTF_8) ⇒ CAConstString .const_string(ca, encoding: Encoding::UTF_8) ⇒ CAConstString .const_string(n, encoding: Encoding::UTF_8) {|i| ... } ⇒ CAConstString

Build a CAConstString (read-only variable-length string column) from Ruby data.

CArray.const_string(["alpha", "", "gamma"])       # 1-D from Array
CArray.const_string(3) { |i| "item#{i}" }         # block form
CArray.const_string([a, nil, b])                   # nil → masked element

B1: "" (length 0) is a valid empty string, distinct from a masked element (nil → masked). B2: element encoding must match :encoding (strict), pure-ASCII strings pass regardless (ASCII-compatible relaxation).

Storage is one (start, end) byte-range pair per element over a pure-concatenation buffer (Arrow string layout). For a high-duplication column (categorical labels), use CACategorical (= Arrow DictionaryArray) instead — CAConstString stores every element's bytes, without dedup.

Overloads:

  • .const_string(values, encoding: Encoding::UTF_8) ⇒ CAConstString

    Returns a read-only CAConstString column packing each String's bytes into a shared buffer. values may be any Array (or Array-like) of Strings; nil entries are masked.

    Parameters:

    • values (Array<String, nil>)

      source values.

    • encoding (Encoding) (defaults to: Encoding::UTF_8)

      column encoding.

    Returns:

  • .const_string(ca, encoding: Encoding::UTF_8) ⇒ CAConstString

    Builds from a string-bearing CArray (String Face / CA_OBJECT / raw CA_FIXLEN, the last read as NUL-stripped strings); always materialises.

    Parameters:

    • ca (CArray)

      source array.

    Returns:

    Raises:

    • (CArray::DataTypeError)

      if ca is numeric / boolean.

  • .const_string(n, encoding: Encoding::UTF_8) {|i| ... } ⇒ CAConstString

    Returns an n-element CAConstString column filled by the block, following the arity-0 broadcast convention.

    Parameters:

    • n (Integer)

      element count.

    Yield Parameters:

    • i (Integer)

      cell index.

    Yield Returns:

    • (String, nil)

      value for cell i.

    Returns:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/carray/const_string.rb', line 53

def self.const_string (arg, encoding: Encoding::UTF_8, &block)
  if arg.is_a?(CArray)
    return string_face_of(arg).to_const_string(encoding: encoding)
  end
  if block
    n = Integer(arg)
    # B5: follow CArray.<type>(n){ ... } arity-0 broadcast quirk for
    #     consistency — arity-0 block is evaluated once and broadcast.
    if block.arity == 0
      v = block.call
      values = Array.new(n) { v }
    else
      values = Array.new(n) { |i| block.call(i) }
    end
  else
    values = arg.to_a
  end

  # Arrow-style layout, built in one C pass: pure-concatenation buffer +
  # one (start,end) int64 pair per element, mask for nil.
  CAConstString.__build__(values, encoding)
end

.dump(ca, **opt) ⇒ String

Returns ca serialized to a String in the _CARRAY3 format.

Parameters:

  • ca (CArray)

    array to serialize.

  • opt (Hash)

    serializer options (:endian).

Returns:

  • (String)


498
499
500
501
502
# File 'lib/carray/serialize.rb', line 498

def self.dump (ca, **opt)
  io = StringIO.new("".b)
  Serializer.new(io).save(ca, **opt)
  return io.string
end

.fixlen_string(values, bytes: nil, truncate: :error) ⇒ CAFixlenString .fixlen_string(ca, bytes: nil, truncate: :error) ⇒ CAFixlenString .fixlen_string(n, bytes: nil, truncate: :error) ⇒ CAFixlenString

Build a CAFixlenString (fixed-width String array over CA_FIXLEN storage).

CArray.fixlen_string(["ab", "cde"], bytes: 4) # explicit slot width CArray.fixlen_string(["ab", "cde"]) # width = max bytesize CArray.fixlen_string([a, nil, b], bytes: 8) # nil → masked element

The bounded slot width is the storage seam CAFixlenString exposes. truncate: controls what happens when a value exceeds bytes (only reachable when bytes is given explicitly; the auto width can never overflow):

:error (default) raise ArgumentError on overflow — loud data loss :silent let the native fixlen store keep the leading bytes bytes

The overflow policy lives at this construction surface, not at per-cell fix[i] = v (which always truncates silently via the native fixlen store). A CArray source is normalised through a String Face (string_face_of): a raw CA_FIXLEN of matching width wraps zero-copy, other string-bearing arrays materialise, numeric is rejected.

Overloads:

  • .fixlen_string(values, bytes: nil, truncate: :error) ⇒ CAFixlenString

    Parameters:

    • values (Array<String, nil>)

      source values.

    • bytes (Integer, nil) (defaults to: nil)

      slot width; defaults to the max bytesize.

    • truncate (Symbol) (defaults to: :error)

      :error or :silent.

    Returns:

  • .fixlen_string(ca, bytes: nil, truncate: :error) ⇒ CAFixlenString

    Parameters:

    • ca (CArray)

      a String Face, CA_OBJECT, or raw CA_FIXLEN array.

    Returns:

    Raises:

    • (CArray::DataTypeError)

      if ca is numeric / boolean.

  • .fixlen_string(n, bytes: nil, truncate: :error) ⇒ CAFixlenString

    Parameters:

    • n (Integer)

      element count.

    • bytes (Integer, nil) (defaults to: nil)

      slot width; defaults to the max bytesize.

    • truncate (Symbol) (defaults to: :error)

      :error or :silent.

    Returns:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/carray/fixlen_string.rb', line 47

def self.fixlen_string (arg, bytes: nil, truncate: :error, &block)
  if arg.is_a?(CArray)
    return string_face_of(arg).to_fixlen_string(bytes: bytes, truncate: truncate)
  end
  unless [:error, :silent].include?(truncate)
    raise ArgumentError, "truncate: must be :error or :silent (got #{truncate.inspect})"
  end
  if block
    n = Integer(arg)
    if block.arity == 0
      v = block.call
      values = Array.new(n) { v }
    else
      values = Array.new(n) { |i| block.call(i) }
    end
  else
    values = arg.to_a
  end

  width = bytes || values.compact.map { |s| s.to_s.bytesize }.max || 1
  width = 1 if width < 1

  if truncate == :error
    values.each_with_index do |s, i|
      next if s.nil?
      b = s.to_s.bytesize
      if b > width
        raise ArgumentError,
              "CArray.fixlen_string: value at #{i} is #{b} bytes, exceeds slot width #{width} " \
              "(use truncate: :silent to keep the leading bytes)"
      end
    end
  end

  entity = CArray.new(CA_FIXLEN, [values.size], :bytes => width)
  values.each_with_index do |s, i|
    entity[i] = s.nil? ? UNDEF : s.to_s
  end
  CAFixlenString.wrap(entity)
end

.format(fmt, *argv) ⇒ CAString

Returns a CAString of formatted strings. Each output cell at index idx is Kernel.format(fmt, *args) where a CArray argument contributes its [*idx] cell and any non-CArray argument is broadcast as-is. The output shape is taken from the first CArray argument; every CArray argument must share that shape.

Parameters:

  • fmt (String)

    Kernel.format template.

  • argv (Array<CArray, Object>)

    per-cell CArrays and/or broadcast scalars.

Returns:

Raises:

  • (ArgumentError)

    when no CArray argument is given, or CArray shapes differ.

Raises:

  • (ArgumentError)


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/carray/methods/string_format.rb', line 18

def self.format (fmt, *argv)
  cas = argv.select { |a| a.is_a?(CArray) }
  raise ArgumentError, "CArray.format: at least one CArray argument is required" if cas.empty?
  shape = cas.first.shape
  cas.each do |a|
    next if a.shape == shape
    raise ArgumentError,
          "CArray.format: shape mismatch (#{a.shape.inspect} vs #{shape.inspect})"
  end
  out = CArray.object(*shape)
  out.map_with_index! do |_, *idx|
    args = argv.map { |a| a.is_a?(CArray) ? a[*idx] : a }
    # a masked cell in any source array masks the output (UNDEF), rather
    # than feeding UNDEF into Kernel.format.
    args.any? { |v| v.equal?(UNDEF) } ? UNDEF : Kernel.format(fmt, *args)
  end
  CAString.wrap(out)
end

.from_bit_string(bstr, nb, data_type = CA_INT32, dim = nil) ⇒ CArray

Returns a new CArray built by unpacking bstr as a packed-bit byte string with nb bits per element.

Parameters:

  • bstr (String)

    packed byte string.

  • nb (Integer)

    bits per element.

  • data_type (Symbol, Integer) (defaults to: CA_INT32)

    result data_type.

  • dim (Array<Integer>, nil) (defaults to: nil)

    result shape; when nil the length is floor(bstr.length * 8 / nb).

Returns:



41
42
43
44
45
46
47
48
49
50
# File 'lib/carray/methods/bit_string.rb', line 41

def self.from_bit_string (bstr, nb, data_type=CA_INT32, dim=nil)
  if dim
    obj = CArray.new(data_type, dim)
  else
    dim0 = ((bstr.length*8)/nb.to_f).floor
    obj = CArray.new(data_type, [dim0])
  end
  obj.from_bit_string(bstr, nb)
  return obj
end

.fuse(*args) {|shadows| ... } ⇒ Object

Runs a transient lazy-fusion scope: wraps each CArray argument with .lazy, yields the wrappers (and any non-CArray args) to the block, then auto-materialises a bare lazy return value into an entity. Non-lazy returns pass through as-is.

Parameters:

Yield Parameters:

  • shadows (Array<CArray, Object>)

    lazy wrappers paired with pass-through non-CArray operands.

Returns:

Raises:

  • (LocalJumpError)

    when no block is given.

Raises:

  • (LocalJumpError)


839
840
841
842
843
844
845
846
847
848
849
# File 'lib/carray/lazy.rb', line 839

def fuse(*args)
  raise LocalJumpError, "CArray.fuse requires a block" unless block_given?
  shadows = args.map { |a| a.is_a?(CArray) ? a.lazy : a }
  result = yield(*shadows)
  case result
  when CAMonOp, CABinOp, CAMonCmp, CABinCmp, CALazyMarker
    result.to_ca
  else
    result
  end
end

.lazy(*args) {|shadows| ... } ⇒ Object

CArray.lazy(*args) { |lazies| ... } — dual of fuse

Like fuse, wraps each CArray argument with .lazy and yields it to the block, but does not auto-materialise at block exit (= returns the lazy structure as-is). If the block return is non-lazy (= Numeric / entity CArray / Array etc.) it's pass-through (= same polymorphic semantics as fuse).

Use cases:

  • Passing a chain between functions: build the lazy expression inside the function and materialise at the caller (= .to_ca / .sum / .mean(axis:) etc.)
  • Reusable expressions: apply the same expr to multiple datasets
  • debug / dump_tree: observe the lazy structure as-is
  • Pick the materialise form later: full materialise or reduction

Example: expr = CArray.lazy(a, b) { |s, o| (s + o) * 2 } expr.class #=> CABinOp (lazy view) expr.to_ca # full materialise expr.sum # reduction (= chain + reduce in 1 pass)

Polymorphic semantics (= symmetric with fuse): CArray.lazy(25.0, b) { |s, o| s + o } # s=25.0 Float pass-through CArray.lazy(arr, b) { |s, o| s + o } # s=arr.lazy

Like fuse but does not auto-materialise: returns whatever the block yields (typically a lazy view) so the expression can be materialised later via .to_ca, .sum, .mean, etc.

Parameters:

Yield Parameters:

Returns:

Raises:

  • (LocalJumpError)

    when no block is given.

Yields:

  • (shadows)

Raises:

  • (LocalJumpError)


884
885
886
887
888
# File 'lib/carray/lazy.rb', line 884

def lazy(*args)
  raise LocalJumpError, "CArray.lazy requires a block" unless block_given?
  shadows = args.map { |a| a.is_a?(CArray) ? a.lazy : a }
  yield(*shadows)
end

.load(input, **opt) ⇒ CArray

Reads a _CARRAY3 payload from input. A String starting with the CArray magic is decoded in place; other Strings are treated as file paths; anything else is used as IO.

Parameters:

  • input (String, IO)

    source path, in-memory payload, or IO.

  • opt (Hash)

    loader options (:data_type).

Returns:



476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/carray/serialize.rb', line 476

def self.load (input, **opt)
  case input
  when String
    if input.bytesize >= Serializer::HEADER_BYTES and
       input.byteslice(0, 8) == Serializer::MAGIC
      io = StringIO.new(input)
      return Serializer.new(io).load(**opt)
    else
      open(input, "rb:ASCII-8BIT") { |io|
        return Serializer.new(io).load(**opt)
      }
    end
  else
    return Serializer.new(input).load(**opt)
  end
end

.load_arrow_tensor(filename) ⇒ CArray

Reads an Arrow tensor IPC file and returns it as a new CArray.

Experimental, and the name is provisional.

Parameters:

  • filename (String)

    path to the message.

Returns:

See Also:



385
386
387
# File 'lib/carray/arrow_tensor.rb', line 385

def self.load_arrow_tensor (filename)
  File.open(filename, "rb") { |io| ArrowTensor.read(io) }
end

.meld(*arrays, axis: 0) ⇒ Object .meld(list, axis: 0) ⇒ CAMeld

Returns a CAMeld view of the arrays welded along an existing axis. No data is copied; reads gather from parents on demand and writes flow back to them (chain composability preserved).

Pieces must agree on ndim, data_type, byte width, and every axis length except axis (the "meld axis"). Mismatched data_type raises: cast the pieces yourself (.to_type(:float64)) or use concatenate (eager, auto-casts).

"meld" = melt + weld — pieces dissolve their boundaries along the named axis and are regarded as one.

Overloads:

  • .meld(list, axis: 0) ⇒ CAMeld

    Convenience form: a single Array argument is treated as the list.

    Parameters:

    • arrays (Array<CArray>)

      pieces to weld. A single Array argument is accepted for compatibility with older callers.

    • axis (Integer) (defaults to: 0)

      existing axis to extend (normalises negative values against the reference ndim).

    Returns:

    • (CAMeld)

      view over the welded pieces.

    Raises:

    • (ArgumentError)

      when the list is empty, ndim mismatch, data_type mismatch, or non-axis dim mismatch across pieces (surfaced by CAMeld.new).

Raises:

  • (ArgumentError)


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/carray/stack.rb', line 89

def self.meld (*arrays, axis: 0)
  if arrays.length == 1 && arrays[0].is_a?(Array)
    arrays = arrays[0]
  end
  raise ArgumentError, "meld: list must not be empty" if arrays.empty?
  first = arrays[0]
  unless first.is_a?(CArray)
    raise ArgumentError, "meld: entries must be CArray (got #{first.class})"
  end
  axis_norm = CArray.normalize_axis(axis, first.ndim, "meld")
  # Flatten nested CAMeld inputs that share our meld axis: they already
  # describe a segment sequence, so absorbing their parents keeps chain
  # depth at 1 (avoids 2-level xfer_all / reduce chains through the
  # intermediate CAMeld).  A CAMeld with a different meld_axis is left
  # intact — its segment structure is orthogonal.
  if arrays.any? { |a| a.is_a?(CAMeld) && a.meld_axis == axis_norm }
    arrays = arrays.flat_map { |a|
      a.is_a?(CAMeld) && a.meld_axis == axis_norm ? a.parents : [a]
    }
  end
  CAMeld.new(arrays, axis: axis_norm)
end

.meshgrid(*axes, indexing: "xy", copy: true, sparse: false) {|grids| ... } ⇒ Array<CArray>

Returns coordinate matrices built from 1-D coordinate vectors.

Given N 1-D vectors, produces N arrays each broadcasting one input axis across the others. Useful for evaluating a function on a grid.

With indexing: "xy" (default) the first two axes are swapped in the output shape (matrix-style convention): meshgrid(x, y) gives outputs of shape [y.elements, x.elements]. With indexing: "ij" input order is preserved: meshgrid(x, y, indexing: "ij") gives outputs of shape [x.elements, y.elements]. For more than two axes only the first two are swapped under "xy"; the remaining axes follow input order in both modes.

When copy is true (default) each output is a materialised CArray; when false, view chains (CARepeat / CAUnboundRepeat) are returned. When sparse is true the outputs are CAUnboundRepeat views that broadcast on demand, saving memory for large grids.

If a block is given, yields the resulting arrays as splat arguments and returns the block's value.

Each axis goes through wrap_readonly, so a coordinate vector may be given as anything that entry point accepts (a CArray, an Array, a Range, a Numeric, a MemoryView producer, an object answering ca / to_ca); its own data type is kept.

Examples:

x = CA_FLOAT64([1.0, 2.0, 3.0])
y = CA_FLOAT64([10.0, 20.0])
xx, yy = CArray.meshgrid(x, y)
xx.shape        # => [2, 3]
yy.to_a         # => [[10.0, 10.0, 10.0], [20.0, 20.0, 20.0]]

Parameters:

  • axes (Array<CArray, Array, Object>)

    1-D coordinate vectors.

  • indexing (String) (defaults to: "xy")

    "xy" or "ij".

  • copy (Boolean) (defaults to: true)

    materialise each output when true.

  • sparse (Boolean) (defaults to: false)

    return broadcast-on-demand views when true.

Yield Parameters:

  • grids (Array<CArray>)

    the resulting coordinate arrays.

Returns:

  • (Array<CArray>)

    the coordinate arrays, or the block's return value.

Raises:

  • (ArgumentError)

    when indexing is neither "xy" nor "ij", or when a coordinate vector is not 1-D.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/carray/methods/meshgrid.rb', line 48

def self.meshgrid (*axes, indexing: "xy", copy: true, sparse: false, &block)
  unless %w[xy ij].include?(indexing)
    raise ArgumentError, %{indexing option should be one of "xy" and "ij"}
  end

  # Each axis is negotiable, so no target type is imposed here; a
  # CArray comes back as itself and anything else is brought in with
  # its own data type.
  axes = axes.map.with_index do |axis, k|
    a = CArray.wrap_readonly(axis)
    unless a.ndim == 1
      raise ArgumentError,
            "coordinate vector #{k} should be 1-D (got #{a.ndim}-D)"
    end
    a
  end

  ndim = axes.size

  # dest[k] = output axis position that input axis k populates.
  # "xy" swaps the first two; everything else is in input order.
  dest = (0...ndim).to_a
  dest[0], dest[1] = 1, 0 if indexing == "xy" && ndim >= 2

  # Output shape: each output axis i takes its size from the input
  # axis that maps there.
  out_shape = Array.new(ndim)
  axes.each_with_index { |a, k| out_shape[dest[k]] = a.size }

  list = axes.map.with_index do |axis, k|
    d = dest[k]
    idx = if sparse
            Array.new(ndim) { |i| i == d ? nil : :* }
          else
            out_shape.dup.tap { |s| s[d] = :% }
          end
    view = axis[*idx]
    copy ? view.copy : view
  end

  block ? block.call(*list) : list
end

.montage(list, tdim, axis: 0, data_type: nil) ⇒ CArray

Arrange list of uniform-shape pieces in a tdim-shape grid that extends parent axes axis..axis+tdim.size-1 by the corresponding tdim[i] factor (= ImageMagick montage analog). Output ndim equals each piece's ndim; the tile axes occupy positions axis..axis+tdim.size-1. Returns a view; call .to_ca to materialise.

tdim.product must equal list.size. For non-uniform pieces along tile axes, use CArray.mosaic.

Example (parent shape (3, 4), 6-element list, tdim=[2, 3], axis: 0):

CArray.montage([a, b, c, d, e, f], [2, 3], axis: 0) #=> shape (6, 12) -- 2 rows x 3 cols grid of (3, 4) blocks # +-----+-----+-----+ # | a | b | c | rows 0..2 # +-----+-----+-----+ # | d | e | f | rows 3..5 # +-----+-----+-----+

3.0 (post K_AXIS / promote_list / stack rename): renamed from combine (= 20-year vocabulary that didn't describe the action). Positional at replaced with axis: kwarg for consistency with bind / stack. Parameter order changed from (tdim, list, at) to (list, tdim, axis:) to align with bind / stack (list first).

Returns a view arranging uniform-shape pieces in a tdim-shape grid that extends parent axes axis..axis+tdim.size-1 by the corresponding tdim[i] factors. Output ndim equals each piece's ndim. tdim.product must equal list.size. For non-uniform pieces along tile axes, use CArray.mosaic.

Parameters:

  • list (Array<CArray>)

    pieces to arrange.

  • tdim (Array<Integer>)

    tile grid shape.

  • axis (Integer) (defaults to: 0)

    first tile axis in the result.

  • data_type (Symbol, Integer, nil) (defaults to: nil)

    result data_type.

Returns:

Raises:

  • (ArgumentError)

    when list is empty or tdim.product != list.size.

Raises:

  • (ArgumentError)


165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/carray/stack.rb', line 165

def self.montage (list, tdim, axis: 0, data_type: nil)
  raise ArgumentError, "montage: list must not be empty" if list.empty?
  unless tdim.is_a?(Array) && tdim.size > 0
    raise ArgumentError, "montage: tdim must be a non-empty Array of Integer"
  end
  expected = tdim.inject(1) { |acc, n| acc * n }
  unless expected == list.size
    raise ArgumentError,
          "montage: tdim product (#{expected}) must equal list size (#{list.size})"
  end

  list = CArray.promote_list(list, data_type: data_type)
  parent_shape = list[0].shape
  ntile = tdim.size
  nparent = parent_shape.size
  axis = CArray.normalize_axis(axis, nparent - ntile + 1, "montage")

  s = CArray.stack(list).reshape(*tdim, *parent_shape)   # (K, *) → (*tdim, *)

  # Interleave: tile axis i (= s axis i, i ∈ [0, ntile)) is moved to
  # just before parent axis (axis + i) in s coordinates (= s axis
  # ntile + axis + i).
  perm = []
  nparent.times do |j|
    if j.between?(axis, axis + ntile - 1)
      perm << (j - axis)           # tile axis
    end
    perm << ntile + j              # parent axis
  end
  s = s.transpose(*perm)

  # Merge each (tile[i], parent[axis+i]) pair via reshape.
  new_shape = parent_shape.dup
  ntile.times { |i| new_shape[axis + i] *= tdim[i] }
  s.reshape(*new_shape)
end

.mosaic(list, tdim, axis: 0, data_type: nil) ⇒ CArray

Returns list tiled into an N-D grid layout described by tdim. Eager (returns a fresh CArray), accepts non-uniform sizes along the tile axes with block-matrix consistency (row-by-row / column-by-column agreement).

Parameters:

  • list (Array<CArray>)

    pieces to tile; length must equal the product of tdim.

  • tdim (Array<Integer>)

    tile grid shape.

  • axis (Integer) (defaults to: 0)

    first tile axis in the result.

  • data_type (Symbol, Integer, nil) (defaults to: nil)

    result data_type; inferred when nil.

Returns:

  • (CArray)

    fresh CArray with tile axes extended by summed per-tile sizes.

Raises:

  • (ArgumentError)

    when list is empty, tdim is ill-formed, or piece shapes violate block-matrix consistency.

Raises:

  • (ArgumentError)


121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/carray/methods/composition.rb', line 121

def self.mosaic (list, tdim, axis: 0, data_type: nil)
  raise ArgumentError, "mosaic: list must not be empty" if list.empty?
  unless tdim.is_a?(Array) && tdim.size > 0
    raise ArgumentError, "mosaic: tdim must be a non-empty Array of Integer"
  end
  expected = tdim.inject(1, :*)
  unless expected == list.size
    raise ArgumentError,
          "mosaic: tdim product (#{expected}) must equal list size (#{list.size})"
  end
  __ragged_paste(list, tdim, axis, data_type)
end

.save(ca, output, **opt) ⇒ CArray

Writes ca to output in the _CARRAY3 portable format. A String output is opened as a binary file; anything else is treated as an IO-like object.

Parameters:

  • ca (CArray)

    array to write.

  • output (String, IO)

    destination path or IO.

  • opt (Hash)

    serializer options (:endian).

Returns:



458
459
460
461
462
463
464
465
466
467
# File 'lib/carray/serialize.rb', line 458

def self.save (ca, output, **opt)
  case output
  when String
    open(output, "wb:ASCII-8BIT") { |io|
      return Serializer.new(io).save(ca, **opt)
    }
  else
    return Serializer.new(output).save(ca, **opt)
  end
end

.select(condlist, choicelist, default: 0, dtype: nil) ⇒ CArray

Multi-way ternary select: for each cell, picks the value from the first choicelist[k] whose matching condlist[k] is true, falling back to default when no condition holds. When several conditions overlap, the earliest one in condlist wins.

All entries in condlist must be same-shape boolean CArrays. Each choicelist[k] is either a same-shape CArray or a scalar broadcast to every cell. The result data_type is the promotion of every choice plus default via CArray.result_type, or dtype when given.

Examples:

x = CArray.float64(6).span(-5.0..5.0)
CArray.select([x < 0, x < 2],
              [-x,    x * 10],
              default: 999)
# => [5.0, 3.0, -10.0, 0.0, 10.0, 999.0]

Parameters:

  • condlist (Array<CArray>)

    boolean selectors.

  • choicelist (Array<CArray, Numeric, Object>)

    values, one per condition (same length as condlist).

  • default (CArray, Numeric, Object) (defaults to: 0)

    value written where no condition holds.

  • dtype (Symbol, Integer, nil) (defaults to: nil)

    override for the result data_type.

Returns:

  • (CArray)

    new array with the shape of condlist[0].

Raises:

  • (ArgumentError)

    on size mismatch, empty condlist, or a non-boolean / wrong-shape entry in condlist.



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/carray/conditional.rb', line 172

def self.select (condlist, choicelist, default: 0, dtype: nil)
  unless condlist.is_a?(Array) && choicelist.is_a?(Array)
    raise ArgumentError, "select: condlist and choicelist must be Arrays"
  end
  if condlist.size != choicelist.size
    raise ArgumentError,
          "select: condlist (#{condlist.size}) and choicelist (#{choicelist.size}) size mismatch"
  end
  if condlist.empty?
    raise ArgumentError, "select: at least one condition required"
  end

  first = condlist.first
  unless first.is_a?(CArray) && first.boolean?
    raise ArgumentError, "select: condlist[0] must be a boolean CArray"
  end
  shape = first.shape

  dt = dtype || CArray.result_type(*choicelist, default)
  # `default` can be either a same-shape CArray (per-cell fallback) or a
  # scalar (broadcast to every cell).
  default_full = default.is_a?(CArray) && !default.scalar?
  out =
    if default_full
      default.data_type == dt ? default.copy : default.to_type(dt)
    else
      CArray.new(dt, shape).fill(default.is_a?(CArray) ? default[0] : default)
    end

  # Iterate from lowest priority to highest (reverse) so the earliest
  # entry in `condlist` ends up on top — matches `np.select`'s
  # first-match semantics.
  (condlist.size - 1).downto(0) do |k|
    c = condlist[k]
    unless c.is_a?(CArray) && c.boolean? && c.shape == shape
      raise ArgumentError,
            "select: condlist[#{k}] must be a same-shape boolean CArray"
    end
    v = choicelist[k]
    out[c] = v.is_a?(CArray) ? v[c] : v
  end
  out
end

.stack(list, axis: 0, data_type: nil) ⇒ CArray

Stack list of CArrays along a new axis inserted at position axis: (default 0 = outermost). Returns a view (CAStack with k_axis = axis) when inputs are storage-uniform, or a Face-lifted view (= CATime, CATimedelta, ...) when inputs are homogeneous Face instances. Call .to_ca to materialise eagerly.

data_type: kwarg (optional, primitive Symbol only) forces primitive promotion; cannot be used when the list contains Face elements. Class / Module targets are rejected (= data_type: CATime is invalid; use auto-detect for Face round-trip).

3.0 (post-K_AXIS, F.S1-stack landed): replaces CArray.merge. The low-level raw constructor is CAStack.new(list, axis:); this method is the high-level surface that performs promote_list + CAStack.new + (face_lift when homogeneous Face).

Returns a view stacking uniform-shape arrays along a new K axis at position axis. Runs promote_list for a common data_type and re-wraps homogeneous Face inputs via face_lift. Output ndim is one greater than each piece.

Parameters:

  • list (Array<CArray>)

    pieces to stack; must not be empty.

  • axis (Integer) (defaults to: 0)

    position of the new K axis.

  • data_type (Symbol, Integer, nil) (defaults to: nil)

    result data_type; inferred when nil.

Returns:

Raises:

  • (ArgumentError)

    when list is empty.

Raises:

  • (ArgumentError)


59
60
61
62
63
64
# File 'lib/carray/stack.rb', line 59

def self.stack (list, axis: 0, data_type: nil)
  raise ArgumentError, "stack: list must not be empty" if list.empty?
  list = CArray.promote_list(list, data_type: data_type)
  axis = CArray.normalize_axis(axis, list[0].ndim + 1, "stack")
  CAStack.new(list, axis: axis)                # CAStack.new does Face lift internally
end

.string(values) ⇒ CAString .string(ca) ⇒ CAString .string(n) {|i| ... } ⇒ CAString

Build a CAString (mutable String array over object storage) from Ruby data.

CArray.string(["alpha", "", "gamma"])   # 1-D from Array
CArray.string(3) { |i| "item#{i}" }     # block form
CArray.string([a, nil, b])              # nil → masked element
CArray.string(other_ca)                 # from a String Face / object / raw fixlen

nil entries become masked cells; "" (empty) is a valid distinct value. A CArray source is normalised through a String Face (string_face_of): a String Face converts, CA_OBJECT storage wraps, a raw CA_FIXLEN reads as NUL-stripped strings; a numeric / boolean array is rejected (stringify with #format / format).

Overloads:

  • .string(values) ⇒ CAString

    Returns a CAString wrapping a CA_OBJECT entity of the given values.

    Parameters:

    • values (Array<String, nil>)

      source values.

    Returns:

  • .string(ca) ⇒ CAString

    Returns a CAString of the string-bearing CArray ca.

    Parameters:

    • ca (CArray)

      a String Face, CA_OBJECT, or raw CA_FIXLEN array.

    Returns:

    Raises:

    • (CArray::DataTypeError)

      if ca is numeric / boolean.

  • .string(n) {|i| ... } ⇒ CAString

    Returns an n-element CAString filled by the block, following the arity-0 broadcast convention.

    Parameters:

    • n (Integer)

      element count.

    Yield Parameters:

    • i (Integer)

      cell index.

    Yield Returns:

    • (String, nil)

      value for cell i.

    Returns:



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/carray/string.rb', line 41

def self.string (arg, &block)
  return string_face_of(arg).to_string if arg.is_a?(CArray)

  if block
    n = Integer(arg)
    if block.arity == 0
      v = block.call
      values = Array.new(n) { v }
    else
      values = Array.new(n) { |i| block.call(i) }
    end
  else
    values = arg.to_a
  end

  entity = CArray.object(values.size)
  values.each_with_index do |s, i|
    entity[i] = s.nil? ? UNDEF : s
  end
  CAString.wrap(entity)
end

.struct(opt = {}) { ... } ⇒ Class

Returns a new CAStruct subclass defined by the block via CAStruct::Builder. Options control alignment, packing, and endianness.

Parameters:

  • opt (Hash) (defaults to: {})

Yields:

Returns:

  • (Class)

    anonymous CAStruct subclass.



533
534
535
# File 'lib/carray/struct.rb', line 533

def self.struct (opt={}, &block)
  return CAStruct::Builder.new(:struct, opt).define(&block)
end

.tabulate(columns, data_type: nil) ⇒ CArray

Returns a 2-D table assembled from a list of column blocks, coerced to a common data_type. Eager (returns a fresh, owned CArray) -- the point is to materialise a typed table, not a view.

Each entry is a 1-D array (one column, length L) or a 2-D array (a block of L x k columns). All entries must share the same length L; tabulate does not pad ragged lengths. Column counts may differ: entries are concatenated along the column axis, so a 1-column, a 3-column and a 2-column block produce a 6-column table. The result data_type is inferred (result_type of the entries) unless data_type is given.

For block-matrix assembly use mosaic; to stack 2-D tables vertically use concatenate(axis: 0).

Examples:

c1 = CA_INT([1, 2, 3])
c2 = CA_DOUBLE([4.5, 5.5, 6.5])
CArray.tabulate([c1, c2])                      # float64 (3, 2)
CArray.tabulate([c1, c2], data_type: :int32)   # int32 (3, 2)

Parameters:

  • columns (Array<CArray>)

    1-D columns and/or 2-D column blocks, all of equal length L.

  • data_type (Symbol, Integer, nil) (defaults to: nil)

    result data_type; inferred when nil.

Returns:

  • (CArray)

    2-D CArray of shape (L, total column count).

Raises:

  • (ArgumentError)

    when columns is empty, entries are not 1-D or 2-D CArrays, or row counts disagree.

Raises:

  • (ArgumentError)


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/carray/methods/composition.rb', line 254

def self.tabulate (columns, data_type: nil)
  raise ArgumentError, "tabulate: columns must not be empty" if columns.empty?
  blocks = columns.map do |c|
    unless c.is_a?(CArray) && (c.ndim == 1 || c.ndim == 2)
      raise ArgumentError, "tabulate: each column must be a 1-D or 2-D CArray"
    end
    c.ndim == 1 ? c[nil, :_] : c          # promote a bare column to (L, 1)
  end
  len = blocks[0].shape[0]
  blocks.each_with_index do |b, i|
    unless b.shape[0] == len
      raise ArgumentError,
            "tabulate: all columns must have equal length (row count) " \
            "(column 0 has length #{len}, column #{i} has length " \
            "#{b.shape[0]}); tabulate does not pad ragged lengths"
    end
  end
  # Equal-length blocks, ragged column counts -> concatenate along the
  # column axis with a common (coerced or inferred) data_type.
  concatenate(blocks, axis: 1, data_type: data_type)
end

.time(x, unit: :s, format: nil, on_error: :raise) ⇒ CATime

Examples:

CArray.time("2024-06-15", unit: :D)                    # 1-element
CArray.time(%w[2024-01-01 2024-02-01], unit: :D)       # Ruby Array
CArray.time(CA_OBJECT(["2024-01-01", "oops"]), unit: :D, on_error: :mask)

Builds a CATime on the unit grid from time value(s). x is either a single literal (Time / ISO 8601 String / Unix-seconds Integer / DateTime) — giving a 1-element result — or a CArray of such literals — giving a same-shape result parsed per cell. Parsing is UTC and DateTime-independent.

A value that cannot be parsed raises by default (on_error: :raise); pass on_error: :mask to make it an UNDEF cell instead. A masked / nil input cell is a missing value (not a parse failure) and always becomes UNDEF, regardless of on_error.

Parameters:

  • x (Time, String, Integer, DateTime, Array, CArray)

    a literal, a Ruby Array of literals, or a CArray of literals.

  • unit (Resolution, Symbol, String) (defaults to: :s)

    target grid resolution.

  • format (String, nil) (defaults to: nil)

    optional strptime format for String input.

  • on_error (:raise, :mask) (defaults to: :raise)

    parse-failure policy (default :raise).

Returns:

  • (CATime)

    shape [1] for a literal, else x's shape.

Raises:

  • (ArgumentError)

    on an unparseable value when on_error: :raise.



1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
# File 'lib/carray/time.rb', line 1637

def self.time(x, unit: :s, format: nil, on_error: :raise)
  res = CATime::Resolution.parse(unit)
  unless %i[raise mask].include?(on_error)
    raise ArgumentError, "on_error: must be :raise or :mask (got #{on_error.inspect})"
  end
  x = CA_OBJECT(x) if x.is_a?(Array)   # Ruby Array of literals -> object CArray
  unless x.is_a?(CArray)
    return _time_cell(x, res, format, on_error)
  end
  raw = CArray.int64(*x.shape)
  x.each_index do |*idx|
    s = x[*idx]
    if s == UNDEF || s.nil?
      raw[*idx] = UNDEF        # missing input -> missing output (no phantom epoch)
      next
    end
    begin
      raw[*idx] = _epoch_tick_index(s, res, format)
    rescue ArgumentError, TypeError
      raise if on_error == :raise
      raw[*idx] = UNDEF        # opt-in parse-mask
    end
  end
  raw.time(unit: res)
end

.time_range(start, last, unit:, step: nil, format: nil) ⇒ CATime

Returns a CATime from start to last inclusive on the unit grid, spaced step apart. unit is the resolution the result is stored on and step is the spacing, so an hourly grid sampled once a day is unit: :h, step: "1 day". With no step the spacing is one unit tick (consecutive ticks). Off-grid endpoints floor to their bucket head (toward the past); the phase is anchored at start, and last is a bound rather than a member -- the series stops at the last step at or before it.

Parameters:

  • start (Time, String, Integer, DateTime)

    first instant.

  • last (Time, String, Integer, DateTime)

    last instant (inclusive).

  • unit (Resolution, Symbol, String)

    grid resolution (tick).

  • step (Resolution, Symbol, String, nil) (defaults to: nil)

    spacing between elements (default: one unit tick). Must be a whole multiple of unit.

  • format (String, nil) (defaults to: nil)

    optional strptime format for String inputs.

Returns:

Raises:

  • (ArgumentError)

    when step is not a whole multiple of unit (including a calendar step on a fixed-length unit, e.g. a month step on an hour grid -- a month is not a fixed number of hours).



1580
1581
1582
1583
1584
1585
1586
1587
1588
# File 'lib/carray/time.rb', line 1580

def self.time_range(start, last, unit:, step: nil, format: nil)
  res    = CATime::Resolution.parse(unit)
  stride = step.nil? ? 1 :
             CATimeUnitAlgebra.multiple_factor(CATime::Resolution.parse(step), res)
  s = _epoch_tick_index(start, res, format)
  e = _epoch_tick_index(last,  res, format)
  n = e < s ? 0 : (e - s) / stride + 1
  CArray.int64(n) {|i| s + i * stride }.time(unit: res)
end

.time_series(start, count:, unit:, step: nil, format: nil) ⇒ CATime

Returns a CATime of count instants starting at start on the unit grid, spaced step apart. unit is the resolution the result is stored on and step is the spacing, so an hourly grid sampled once a day is unit: :h, step: "1 day". With no step the spacing is one unit tick (consecutive ticks, as before).

Parameters:

  • start (Time, String, Integer, DateTime)

    first instant.

  • count (Integer)

    number of elements.

  • unit (Resolution, Symbol, String)

    grid resolution (tick).

  • step (Resolution, Symbol, String, nil) (defaults to: nil)

    spacing between elements (default: one unit tick). Must be a whole multiple of unit.

  • format (String, nil) (defaults to: nil)

    optional strptime format for String inputs.

Returns:

Raises:

  • (ArgumentError)

    when step is not a whole multiple of unit (including a calendar step on a fixed-length unit, e.g. a month step on an hour grid -- a month is not a fixed number of hours).



1606
1607
1608
1609
1610
1611
1612
# File 'lib/carray/time.rb', line 1606

def self.time_series(start, count:, unit:, step: nil, format: nil)
  res    = CATime::Resolution.parse(unit)
  stride = step.nil? ? 1 :
             CATimeUnitAlgebra.multiple_factor(CATime::Resolution.parse(step), res)
  s = _epoch_tick_index(start, res, format)
  CArray.int64(count) {|i| s + i * stride }.time(unit: res)
end

.union(opt = {}) { ... } ⇒ Class

Returns a new CAUnion subclass defined by the block. Same options and DSL as struct but every member occupies the same offset.

Parameters:

  • opt (Hash) (defaults to: {})

Yields:

Returns:

  • (Class)

    anonymous CAUnion subclass.



544
545
546
# File 'lib/carray/struct.rb', line 544

def self.union (opt={}, &block)
  return CAStruct::Builder.new(:union, opt).define(&block)
end

Instance Method Details

#<=>(other) ⇒ CArray Also known as: cmp

Returns an element-wise 3-way comparison: +1 where self > other, -1 where self < other, 0 where equal. Output data_type is CA_INT8.

Parameters:

  • other (CArray, Numeric)

    operand to compare against.

Returns:



243
244
245
# File 'lib/carray/basics.rb', line 243

def <=> (other)
  (self > other).as_int8 - (self < other).as_int8
end

#__real_eager__Object


real / imag lazy fuse (post-IC follow-up):

Eager real/imag in lib/carray/math.rb return CAField views (zero-copy, mutable byte-offset access into complex storage) or fresh template entities for non-complex. Both break a lazy chain when applied to a lazy parent (CAField materialises the parent into an entity to byte-offset into). For lazy parents we re-express via chain composition so the chain stays fused:

complex parent .real → CAMonOp(cast_) (cmplx128→f64 / cmplx64→f32 casts pick up the real part — verified equivalent to existing CAField path for read-only consumption)

complex parent .imag → CAMonOp(cast_) ∘ CAMonOp(imag_i) (imag_i puts cimag in the real component of a same-data_type slot, cast then extracts it as float — same trick as abs)

non-complex parent .real → self (= pass-through, chain unchanged)

non-complex parent .imag → CAMonOp(imag_i) (= same-shape lazy zero)

Eager parents fall through to the existing math.rb implementation (= CAField for complex, CARefer/template for non-complex) so the mutable-setter use cases (.real = val / .imag = val) remain supported. Lazy views are inherently read-only so the loss of

mutability on the lazy path is not a regression.



177
# File 'lib/carray/lazy.rb', line 177

alias_method :__real_eager__, :real

#addressCArray

Returns an int32 CArray of the same shape as self where each cell holds its row-major flat address.

Returns:



152
153
154
# File 'lib/carray/basics.rb', line 152

def address
  return CArray.int32(*shape).seq!
end

#all(skip_masked: true, **opts) ⇒ Boolean, CArray

Whether every cell is true.

With skip_masked: true (the default) masked cells are simply ignored and the result is always true / false. With skip_masked: false the fold is three-valued: the result is UNDEF when a masked cell could change it, matching the element-wise Kleene semantics of | / &.

Parameters:

  • skip_masked (Boolean) (defaults to: true)

    ignore masked cells, or fold them three-valued.

  • opts (Hash)

    forwarded to the underlying reduction (axis:, keep_axis:, ...).

Returns:

  • (Boolean, CArray)

    a scalar, or an array when an axis is given.



54
55
56
57
# File 'lib/carray/boolean_reduce.rb', line 54

def all (skip_masked: true, **opts)
  return __all_skipna__(**opts) if skip_masked
  __kleene_fold(:all, opts)
end

#any(skip_masked: true, **opts) ⇒ Boolean, CArray

Whether any cell is true.

With skip_masked: true (the default) masked cells are simply ignored and the result is always true / false. With skip_masked: false the fold is three-valued: the result is UNDEF when a masked cell could change it, matching the element-wise Kleene semantics of | / &.

Parameters:

  • skip_masked (Boolean) (defaults to: true)

    ignore masked cells, or fold them three-valued.

  • opts (Hash)

    forwarded to the underlying reduction (axis:, keep_axis:, ...).

Returns:

  • (Boolean, CArray)

    a scalar, or an array when an axis is given.



38
39
40
41
# File 'lib/carray/boolean_reduce.rb', line 38

def any (skip_masked: true, **opts)
  return __any_skipna__(**opts) if skip_masked
  __kleene_fold(:any, opts)
end

#attr(key) ⇒ Object?

Returns the value of the attribute key, or nil when the key is absent. Walks the parent chain per key: the deepest view that has an entry for key wins.

Parameters:

  • key (Symbol, String)

    attribute key.

Returns:



41
42
43
44
45
46
47
# File 'lib/carray/attribute.rb', line 41

def attr (key)
  k = attr_normalize_key(key)
  attr_each_chain do |h|
    return h[k] if h.key?(k)
  end
  nil
end

#attrsHash{String => Object}

Returns a frozen shallow Hash of all attributes visible on self, merged along the parent chain (deeper writes shadow shallower ones on a per-key basis).

Returns:



69
70
71
72
73
74
75
76
# File 'lib/carray/attribute.rb', line 69

def attrs
  merged = nil
  attr_each_chain do |h|
    merged ||= {}
    h.each { |k, v| merged[k] = v unless merged.key?(k) }
  end
  (merged || {}).freeze
end

#axis_group(*slots) ⇒ Object


CArray#axis_group(cat_or_nil, ...) -- build an AxisGroup spec.

Slot position = source axis. A CACategorical slot consumes cat.ndim source axes (rank-1 = one axis, rank-N = several axes collapsed into one group axis); a nil slot is a band (held) axis. ALL axes must be given explicitly -- the rank-sum must equal self.ndim, trailing omission / nil fill is forbidden (explicit > implicit). The value is used as a shape TEMPLATE only (its data is never read).



33
34
35
# File 'lib/carray/axis_group.rb', line 33

def axis_group (*slots)
  AxisGroup.new(self, slots)
end

#bin(vmin, vmax, step = nil, bins: nil, lfill: nil, ufill: nil, include_max: true) ⇒ CArray

Returns each element's bin index for equal-width, half-open bins over [vmin, vmax]. Bin k covers [vmin + k*w, vmin + (k+1)*w) where w = (vmax - vmin) / n. The number of bins is set either by step (positional; bin width, matching snap's step) or by bins: (kwarg; count of bins, matching histogram convention); exactly one must be given.

Because [vmin, vmax] is a user-declared inclusive range, values exactly equal to vmax land in the last bin by default (include_max: true); this differs from bin_to, where the user-supplied edges are treated as-is (half-open, default false).

Out-of-range convention follows bin_to / project: lfill for below-range, ufill for above-range; nil on either side masks that side. NaN / masked input cells are always masked in the output, independently of lfill / ufill.

Use snap(step, offset:) when the desired output is the snapped value; use bin_to(edges) when the edges are non-uniform.

Examples:

temp.bin(270, 300, 0.5)               # 60 uniform bins every 0.5 K
temp.bin(0, 1, bins: 100)             # 100 equal-width bins over [0, 1]
temp.bin(0, 9, 1, lfill: 0, ufill: 8) # clamp OOB to end bins

Parameters:

  • vmin (Numeric)

    range lower bound.

  • vmax (Numeric)

    range upper bound; must be >= vmin.

  • step (Numeric, nil) (defaults to: nil)

    bin width. The number of bins is ((vmax - vmin) / step).round (silent FP rounding, standard numeric convention).

  • bins (Integer, nil) (defaults to: nil)

    number of bins (alternative to step); must be >= 1.

  • lfill (Integer, nil) (defaults to: nil)

    fill for below-range cells.

  • ufill (Integer, nil) (defaults to: nil)

    fill for above-range cells.

  • include_max (Boolean) (defaults to: true)

    fold values equal to vmax into the last bin instead of treating them as above-range.

Returns:

  • (CArray)

    CA_INT64 bin indices in [0, n-1], same shape as self.

Raises:

  • (ArgumentError)

    when neither / both of step and bins: are given, bins < 1, or vmin > vmax.

Raises:

  • (ArgumentError)


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/carray/methods/bin.rb', line 43

def bin(vmin, vmax, step = nil, bins: nil, lfill: nil, ufill: nil, include_max: true)
  if step.nil? == bins.nil?
    raise ArgumentError, "bin: give exactly one of `step` or `bins:`"
  end
  raise ArgumentError, "bin: vmin > vmax" if vmin > vmax

  n = bins || ((vmax - vmin).to_f / step).round
  raise ArgumentError, "bin: n must be >= 1" if n < 1

  if vmin == vmax
    # Degenerate: zero interval → all cells fall on the single edge;
    # with include_max: true they land in bin 0.
    out = CArray.int64(*shape) { 0 }
    out.mask = self.mask.to_ca if self.has_mask?
    if self.float?
      inv = self.is_invalid
      if inv.count(true) > 0
        out.mask = out.has_mask? ? (out.mask | inv) : inv
      end
    end
    return out
  end

  # Delegate to `bin_to` with generated uniform edges — same kernel
  # (`histbin_ki`) as `histogram`, so semantics are identical.
  edges = CArray.float64(n + 1).span(vmin..vmax)
  bin_to(edges, lfill: lfill, ufill: ufill, include_max: include_max)
end

#bin_to(edges, lfill: nil, ufill: nil, include_max: false) ⇒ CArray

Returns each element's bin index against an explicit ascending edges array (non-uniform binning). Sibling of bin (uniform, range + step) and snap_to (nearest-value snap to the same shape of grid).

edges are N+1 ascending boundaries defining N bins; bin k covers the half-open interval [edges[k], edges[k+1]). Out-of-range values follow the bin / project convention: a value below edges[0] becomes lfill, a value at or above edges[-1] becomes ufill; nil on either side masks that side. When include_max is true, a value exactly equal to edges[-1] lands in the last bin N-1 instead of being treated as above-range. NaN or masked input cells are masked in the output, independently of lfill / ufill.

The inner binning kernel is shared with histogram, which counts how many values land in each bin.

Examples:

e = CA_FLOAT64([0, 1, 10, 100])
v = CA_FLOAT64([0.5, 5.0, 50.0, -1.0, 200.0])
v.bin_to(e)                  # => [0, 1, 2, UNDEF, UNDEF]
v.bin_to(e, lfill: 0, ufill: 2)
                             # => [0, 1, 2, 0, 2]

Parameters:

  • edges (CArray, Array<Numeric>)

    1-D ascending boundaries with at least 2 values.

  • lfill (Integer, nil) (defaults to: nil)

    fill for below-range cells; nil masks them.

  • ufill (Integer, nil) (defaults to: nil)

    fill for above-range cells; nil masks them.

  • include_max (Boolean) (defaults to: false)

    fold values equal to edges[-1] into the last bin instead of treating them as above-range.

Returns:

  • (CArray)

    CA_INT64 array with the same shape as self holding bin indices in [0, N-1] (or the fill values / mask for out-of-range and masked cells).

Raises:

  • (ArgumentError)

    when edges is not 1-D or has fewer than 2 values.

Raises:

  • (ArgumentError)


110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/carray/methods/bin.rb', line 110

def bin_to(edges, lfill: nil, ufill: nil, include_max: false)
  e = CArray.wrap_readonly(edges, :float64)
  raise ArgumentError, "bin_to: edges must be 1-D" unless e.ndim == 1
  raise ArgumentError, "bin_to: edges needs at least 2 values" if e.elements < 2
  n = e.elements - 1                                  # number of bins

  src = data_type == CA_FLOAT64 ? self : CArray.wrap_readonly(self, :float64)

  # histbin_ki returns the extended index (0 = under, 1..N = in-range bins,
  # N+1 = over; NaN / masked -> masked).  Shift to the in-range convention:
  # under -> -1, in-range -> 0..N-1, over -> N.
  out = src.send(:histbin_ki, e, include_max) - 1

  out[:eq, -1] = lfill.nil? ? UNDEF : lfill          # under
  out[:eq, n]  = ufill.nil? ? UNDEF : ufill          # over
  out
end

#bincount(weights: nil, length: 0) ⇒ CArray

Returns occurrence counts per non-negative integer label in self, or a per-label sum of weights.

Masked labels are skipped (not counted). Masked weights are skipped too (their label contributes 0).

Examples:

labels = CA_INT32([0, 1, 1, 2, 0, 1])
labels.bincount                    # => CA_UINT32([2, 3, 1])
labels.bincount(length: 5)         # => CA_UINT32([2, 3, 1, 0, 0])
weights = CA_DOUBLE([1, 2, 3, 4, 5, 6])
labels.bincount(weights: weights)  # => CA_DOUBLE([6, 11, 4])

Parameters:

  • weights (CArray, nil) (defaults to: nil)

    when given, sums weights per label instead of counting; length must equal self.elements and the output data_type is inherited from weights. When nil (default), counts occurrences and returns CA_UINT32 (or CA_UINT64 if length >= 2^32).

  • length (Integer) (defaults to: 0)

    minimum output length; the actual length is max(length, self.max + 1).

Returns:

  • (CArray)

    1-D output of length max(length, self.max + 1).

Raises:

  • (CArray::DataTypeError)

    when self is not an integer data_type.

  • (ArgumentError)

    when a label is negative or weights length disagrees with self.elements.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/carray/methods/bincount.rb', line 32

def bincount(weights: nil, length: 0)
  unless [CA_INT8, CA_INT16, CA_INT32, CA_INT64,
          CA_UINT8, CA_UINT16, CA_UINT32, CA_UINT64].include?(data_type)
    raise CArray::DataTypeError,
          "bincount requires an integer label array (got #{data_type_name})"
  end

  if elements.zero?
    if weights
      out = CArray.new(weights.data_type, [length])
    else
      out_type = (length > 0xFFFFFFFF) ? CA_UINT64 : CA_UINT32
      out = CArray.new(out_type, [length])
    end
    out.fill(0) unless length.zero?
    return out
  end

  # Single-pass fused min+max so the prereq scan over labels costs
  # one walk instead of two.
  label_min, label_max = minmax
  if label_min.equal?(UNDEF)
    # Every cell is masked: no labels to count, same result as an empty
    # input (all-zero output of the requested minimum length).
    if weights
      out = CArray.new(weights.data_type, [length])
    else
      out_type = (length > 0xFFFFFFFF) ? CA_UINT64 : CA_UINT32
      out = CArray.new(out_type, [length])
    end
    out.fill(0) unless length.zero?
    return out
  end
  if label_min < 0
    raise ArgumentError,
          "bincount: negative label not allowed (got #{label_min})"
  end

  n = [length, label_max + 1].max

  if weights
    unless weights.is_a?(CArray)
      raise ArgumentError, "bincount: weights must be a CArray"
    end
    if weights.elements != elements
      raise ArgumentError,
            "bincount: weights length (#{weights.elements}) doesn't " \
            "match labels length (#{elements})"
    end
    __bincount_weighted__(weights, n)
  else
    __bincount_count__(n)
  end
end

#bincount_nd(lengths:, axis: [-2, -1], weights: nil) ⇒ BincountND

Returns a discrete N-D joint BincountND count of self with shape fiber_shape + (A, M). Each of the M channels is an integer label in 0..lengths[k]-1; labels >= lengths[k] fold into the upper overflow cell, negative labels raise.

Parameters:

  • lengths (Array<Integer>)

    per-dimension extents.

  • axis (Array(Integer, Integer)) (defaults to: [-2, -1])

    [sample, channel] axis pair.

  • weights (CArray, nil) (defaults to: nil)

    optional per-sample weights.

Returns:

Raises:

  • (ArgumentError)


339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/carray/bincount_nd.rb', line 339

def bincount_nd (lengths:, axis: [-2, -1], weights: nil)
  raise ArgumentError, "lengths must be an Array of per-dim extents" unless lengths.is_a?(Array)
  sample_ax  = normalize_axis(axis[0], "bincount_nd sample axis")
  channel_ax = normalize_axis(axis[1], "bincount_nd channel axis")
  fiber_shape = shape.dup
  [sample_ax, channel_ax].sort.reverse.each { |p| fiber_shape.delete_at(p) }

  # Weighted counts are float64-only (the FLAT bincount coerces weights to the
  # counts dtype and the FIBER kernel requires float64 weights/counts), so the
  # dtype is fixed here rather than derived from the weights' own dtype.
  weights_dtype = (:float64 if weights)

  h = BincountND.send(:new,
                      lengths: lengths,
                      fiber_shape: fiber_shape,
                      weights_dtype: weights_dtype)
  h.add(self, axis: axis, weights: weights)
  h
end

#blocks(*blocks) ⇒ CABlockIterator

Returns a CABlockIterator tiling self with non-overlapping tiles of a per-axis size. Each argument is an Integer tile size (offset 0) or a lo..hi range (length = tile size, start = leading offset). The remainder is covered by present-only edge tiles (ceil tile grid); slice first for "valid" tiling.

Parameters:

  • blocks (Array<Integer, Range>)

    per-axis tile sizes (or ranges).

Returns:



601
602
603
# File 'lib/carray/block_iterator.rb', line 601

def blocks (*blocks)
  CABlockIterator.new(self, *blocks)
end

#categorize(labels: nil, sort_labels: false) ⇒ CACategorical

Build a CACategorical from self read as category keys (= the values whose distinct levels become the categories). Codes are dense 0-based in the order labels appear (first-appearance by default, or ascending sorted when sort_labels: true); masked keys become masked (excluded) codes.

labels: nil -> discover, first-appearance order labels: nil, sort_labels: true -> discover, then sort ascending labels: set -> fixed vocabulary (must be unique); keys outside it are excluded (masked)

Returns a CACategorical built from self read as category keys. With labels: nil distinct levels are discovered in first-appearance order (or ascending sorted when sort_labels: true); with an explicit labels list the vocabulary is fixed and keys outside it become masked (excluded). sort_labels: is ignored when an explicit labels is given (the caller has already chosen the order).

Parameters:

  • labels (Array, CArray, nil) (defaults to: nil)

    fixed vocabulary; nil enables discovery.

  • sort_labels (Boolean) (defaults to: false)

    when discovering (labels: nil), sort the discovered vocabulary ascending after collecting it.

Returns:

Raises:

  • (ArgumentError)

    when explicit labels contain duplicates.



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# File 'lib/carray/categorical.rb', line 515

def categorize(labels: nil, sort_labels: false)
  # Automatic appearance-order vocabulary: one linear pass (C
  # __factorize_appearance__) returns both codes and levels directly, over the
  # integer / float / object / fixlen / boolean lanes (boolean rides the uint8
  # lane). Distinctness is the hash-key judgement shared with the discovery
  # family: Float NaN collapses to one category and -0.0 == +0.0, while mixed
  # Integer / Float keys stay distinct (eql?, so 1 and 1.0 are separate
  # categories). The discovery path below is reserved for sort_labels (which
  # reorders the vocabulary, desyncing the appearance-order codes), an explicit
  # labels list, and the dtypes the factorize kernel does not take (complex).
  if labels.nil? && !sort_labels && (integer? || float? || object? || fixlen? || boolean?)
    codes, levels = __factorize_appearance__
    return CACategorical.from_codes(codes, levels.to_a)
  end

  if labels.nil?
    # Discover the levels in first-appearance order: mask_duplicates keeps the
    # first occurrence of each distinct value and masks the rest (already-
    # masked keys stay excluded), so the non-masked cells are the levels.
    # Only the final list is Ruby, since labels are Ruby objects.
    labels_arr = mask_duplicates[:is_not_masked].to_a
    labels_arr.sort! if sort_labels
  else
    labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels)
    if labels_arr.uniq.size != labels_arr.size
      raise ArgumentError, "categorize: labels: must be unique (got duplicates)"
    end
  end

  # Choose a narrow unsigned code dtype, reserving its top value as the
  # exclusion sentinel so it never collides with a real code 0..k-1.
  k = labels_arr.size
  code_type, sentinel =
    if    k <= 0xFF   then [CA_UINT8,  0xFF]
    elsif k <= 0xFFFF then [CA_UINT16, 0xFFFF]
    else                   [CA_UINT32, 0xFFFFFFFF]
    end

  # One vectorized masked write per category. Cells matching no category
  # (out-of-vocabulary) and masked cells (eq yields UNDEF, skipped) keep the
  # sentinel; from_codes then derives the mask from it.
  codes = CArray.new(code_type, shape).fill(sentinel)
  labels_arr.each_with_index { |label, c| codes[eq(label)] = c }

  CACategorical.from_codes(codes, labels_arr)
end

#choose(choices, data_type: nil) ⇒ CArray

Returns label-based per-cell selection: self is an integer label array, and choices is a list indexed by those labels.

Where self == i, the result takes choices[i] -- a scalar fills those cells, a CArray contributes its corresponding cells. The result has the same shape as self.

Examples:

ref = CA_INT([[0, 1, 2], [1, 2, 0], [2, 0, 1]])
a = CArray.int(3, 3).seq(1)
b = CArray.int(3, 3).seq(11)
c = CArray.int(3, 3).seq(21)
ref.choose([a, b, c])       # per-cell pick from a / b / c
ref.choose(["a", "b", "c"]) # recode labels to values

Parameters:

  • choices (Array<CArray, Object>)

    values indexed by the labels in self; each entry is either a same-shape CArray or a scalar fill.

  • data_type (Symbol, Integer, nil) (defaults to: nil)

    result data_type. When nil it is inferred: CArray.result_type of the CArray choices, or CA_OBJECT when every choice is a scalar.

Returns:

  • (CArray)

    new CArray with the shape of self holding the chosen values.



26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/carray/methods/choose.rb', line 26

def choose (choices, data_type: nil)
  unless data_type
    ca = choices.select { |v| v.is_a?(CArray) }
    data_type = ca.empty? ? CA_OBJECT : CArray.result_type(*ca)
  end
  out = template(data_type)
  choices.each_with_index do |v, i|
    s = self.eq(i)
    out[s] = v.is_a?(CArray) ? v[s] : v
  end
  out
end

#clip(min, max = nil, fill_value = nil, lfill: nil, ufill: nil) ⇒ CArray

Returns self with every element clamped to [min, max].

Either bound may be nil for a one-sided clip; that side dispatches to the pmax / pmin binop kernels. When fill_value (or lfill / ufill) is given, out-of-range cells are replaced by the fill instead of clamped -- pass UNDEF to mask that end. fill_value is sugar for symmetric dual-fill; lfill / ufill override per side.

Boundary is strict [min, max]: values equal to a bound remain unchanged in both the clamped and filled variants.

Examples:

a.clip(0, 10)                          # strict clamp
a.clip(0, 10, -1)                      # both ends -> -1
a.clip(0, 10, lfill: UNDEF, ufill: 99) # below masks

Parameters:

  • min (Numeric, nil)

    lower bound; nil for one-sided clip above.

  • max (Numeric, nil) (defaults to: nil)

    upper bound; nil for one-sided clip below.

  • fill_value (Object, nil) (defaults to: nil)

    symmetric fill for out-of-range cells.

  • lfill (Object, nil) (defaults to: nil)

    override below-range fill.

  • ufill (Object, nil) (defaults to: nil)

    override above-range fill.

Returns:

  • (CArray)

    new CArray with clamped or filled values.

Raises:

  • (ArgumentError)

    when both min and max are nil.



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/carray/basics.rb', line 276

def clip(min, max=nil, fill_value=nil, lfill: nil, ufill: nil)
  if min.nil? && max.nil?
    raise ArgumentError, "clip: at least one of (min, max) must be given"
  end

  # `fill_value` as a single argument is sugar applied to both ends; kwargs override.
  lfill = fill_value if lfill.nil?
  ufill = fill_value if ufill.nil?

  if lfill.nil? && ufill.nil?
    return __clip_ki__(min, max) if !min.nil? && !max.nil?
    return pmax(min) if max.nil?
    return pmin(max)
  end

  out = self.copy
  out[:lt, min] = lfill unless min.nil? || lfill.nil?
  out[:gt, max] = ufill unless max.nil? || ufill.nil?
  out
end

#coerceObject Also known as: __coerce_eager__



1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
# File 'ext/carray_operator.c', line 1800

static VALUE
rb_ca_coerce (VALUE self, VALUE other)
{
  if ( rb_obj_is_carray(other) ) {
    return Qnil;
  }
  else if ( rb_respond_to(other, rb_intern("to_ca")) ) {
    return rb_ca_coerce(self, rb_funcall(other,rb_intern("to_ca"),0));
  }
  else {
    /* do implicit casting and resolving unbound repeat array */
    rb_ca_cast_self_or_other(&self, &other);
    return rb_assoc_new(other, self);
  }
}

#concatenate(*others, axis: 0, data_type: nil) ⇒ CArray

Instance form of concatenate: returns [self, *others] concatenated along axis as a fresh CArray. Eager auto-cast counterpart of #meld (view).

Parameters:

  • others (Array<CArray>)

    additional pieces.

  • axis (Integer) (defaults to: 0)

    axis to concatenate along.

  • data_type (Symbol, Integer, nil) (defaults to: nil)

    result data_type; inferred via result_type when nil.

Returns:

Raises:

  • (ArgumentError)

    when no others are given.

Raises:

  • (ArgumentError)


100
101
102
103
# File 'lib/carray/methods/composition.rb', line 100

def concatenate (*others, axis: 0, data_type: nil)
  raise ArgumentError, "concatenate: at least one other array required" if others.empty?
  CArray.concatenate([self, *others], axis: axis, data_type: data_type)
end

#conditional(cond, then_fn, else_fn, dtype: nil) ⇒ CArray

Returns per-cell then_fn.call(self[cond]) where cond is true and else_fn.call(self[cond.not]) where it is false. The two callables are applied only to their own subset of self, so a branch that would fail on the other region (e.g. ->(v) { v.log } on negative cells) stays safe.

Scalar returns from a callable (e.g. ->(v) { 0 }) broadcast to the subset shape. The result data_type is the promotion of the two subset results via CArray.result_type, or dtype when given. Masked cells in cond propagate to UNDEF in the result.

Examples:

x = CArray.float64(6).span(-2.0..3.0)
x.conditional(x > 0,
              ->(v) { v.log },        # domain-safe: only positive cells
              ->(v) { -v })
# => [2.0, 1.0, -0.0, 0.0, 0.6931..., 1.0986...]

Parameters:

  • cond (CArray)

    boolean selector; same shape as self.

  • then_fn (#call)

    callable applied to self[cond].

  • else_fn (#call)

    callable applied to self[cond.not].

  • dtype (Symbol, Integer, nil) (defaults to: nil)

    override for the result data_type.

Returns:

  • (CArray)

    new array with the same shape as self.

Raises:

  • (ArgumentError)

    when cond is not a same-shape boolean CArray.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/carray/conditional.rb', line 111

def conditional (cond, then_fn, else_fn, dtype: nil)
  unless cond.is_a?(CArray) && cond.boolean? && cond.shape == self.shape
    raise ArgumentError,
          "conditional: cond must be a boolean CArray with same shape as self"
  end

  x_then = self[cond]
  x_else = self[cond.not]
  y_then = then_fn.call(x_then)
  y_else = else_fn.call(x_else)

  # A callable that returns a scalar (e.g. `->(v) { 0 }`) broadcasts
  # to the subset shape; wrap it here so the scatter step below sees a
  # same-length CArray.
  unless y_then.is_a?(CArray)
    y_then = CArray.new(dtype || CArray.result_type(y_then), x_then.shape).fill(y_then)
  end
  unless y_else.is_a?(CArray)
    y_else = CArray.new(dtype || CArray.result_type(y_else), x_else.shape).fill(y_else)
  end

  dt  = dtype || CArray.result_type(y_then, y_else)
  out = CArray.new(dt, self.shape)
  out[cond]     = y_then
  out[cond.not] = y_else
  # Propagate cond's mask (mirrors then_else's rule): UNDEF in cond ->
  # UNDEF in out.  Kleene `cond.not` also carries UNDEF at the same
  # positions, so both scatters leave the cell untouched — an explicit
  # fix-up is required.
  out[cond.is_masked] = UNDEF if cond.has_mask?
  out
end

#crop(offset, dst) ⇒ CArray

Reads a dst.shape-sized region from self starting at offset into dst. Cells whose read position falls outside self leave the corresponding dst cells untouched.

Parameters:

  • offset (Array<Integer>)

    source starting indices, length equal to self.ndim.

  • dst (CArray)

    destination array; mutated in place.

Returns:

Raises:

  • (ArgumentError)

    when offset.length != self.ndim.

Raises:

  • (ArgumentError)


203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/carray/basics.rb', line 203

def crop (offset, dst)
  raise ArgumentError, "offset length must equal ndim" if offset.length != ndim
  src_ranges = []
  dst_ranges = []
  ndim.times do |i|
    s_lo = [offset[i], 0].max
    s_hi = [offset[i] + dst.shape[i], shape[i]].min
    return dst if s_lo >= s_hi
    src_ranges << (s_lo...s_hi)
    dst_ranges << ((s_lo - offset[i])...(s_hi - offset[i]))
  end
  dst[*dst_ranges] = self[*src_ranges]
  dst
end

#delete_block(offset, bsize) ⇒ CArray

Returns a new CArray obtained by deleting a block of bsize cells (per axis) starting at offset, shrinking the array.

Per axis, offset accepts 0..shape[i]-1 (negative counts from the end) and bsize[i] must be non-negative with offset[i] + bsize[i] <= shape[i]. Bytes and Face identity are preserved naturally because the result is built by fancy-index copy of self. The offset array is not mutated.

Parameters:

  • offset (Array<Integer>)

    start of the block per axis.

  • bsize (Array<Integer>)

    block size per axis.

Returns:

Raises:

  • (ArgumentError)

    on ndim mismatch or out-of-range offset / size.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/carray/methods/insert_block.rb', line 76

def delete_block (offset, bsize)
  if offset.size != ndim or bsize.size != ndim
    raise ArgumentError, "ndim mismatch"
  end
  offset = offset.dup           # normalize without mutating the caller's array
  newdim = shape
  grids  = []
  ndim.times do |i|
    offset[i] += shape[i] if offset[i] < 0
    if bsize[i] < 0 or offset[i] < 0 or offset[i] >= shape[i] or
        offset[i] + bsize[i] > shape[i]
      raise ArgumentError, "invalid offset or size at axis #{i}"
    end
    newdim[i] -= bsize[i]
    grids[i] = CArray.int32(newdim[i])
    grids[i][0...offset[i]].seq! if offset[i] > 0
    if offset[i] + bsize[i] < shape[i]
      grids[i][offset[i]..-1].seq!(offset[i]+bsize[i])
    end
  end
  return self[*grids].copy
end

#difference(other, sort: false) ⇒ CArray

Returns a 1-D CArray of the distinct values in self that are absent from other, in self's first-appearance order. See #intersection for the shared semantics and options.

Parameters:

  • other (CArray, Array, Range)

    promoted with self.

  • sort (Boolean) (defaults to: false)

    when true, return the values sorted ascending.

Returns:

  • (CArray)

    1-D CArray of the self-only distinct values.



75
76
77
78
79
# File 'lib/carray/methods/is_in.rb', line 75

def difference (other, sort: false)
  a, b = promote_value_set(other)
  r = a.__send__(:__difference__, b)
  sort ? r.sort : r
end

#drop_axisCArray

Returns a view of self with every size-1 axis dropped.

Returns:

  • (CArray)

    view with reduced ndim.



139
140
141
142
143
144
145
146
# File 'lib/carray/basics.rb', line 139

def drop_axis
  if ndim == 1
    return self[]
  else
    newdim = shape.reject{|x| x == 1 }
    return ( ndim != newdim.size ) ? reshape(*newdim) : self[]
  end
end

#falseCArray

Returns a boolean CArray of the same shape as self filled with false.

Returns:



160
161
162
# File 'lib/carray/basics.rb', line 160

def false ()
  return template(:boolean)
end

#format(fmt, *argv) ⇒ CAString

Returns a CAString formatting each cell of self with fmt; self is the first Kernel.format argument, so arr.format("%03d") renders the cells and arr.format("%s=%d", other) interleaves a second array. This is the explicit stringify path for numeric (and any) arrays — the to_* string conversions are String-Face only. Equivalent to CArray.format(fmt, self, *argv).

Defining a public CArray#format shadows the private Kernel#format for CArray instances; internal CArray methods therefore use sprintf / Kernel.format explicitly (verified: no bare format(...) call runs on a CArray receiver anywhere in the library).

Parameters:

  • fmt (String)

    Kernel.format template.

  • argv (Array<CArray, Object>)

    additional per-cell CArrays / broadcast scalars.

Returns:

Raises:

  • (ArgumentError)

    when a CArray argument's shape differs from self.



53
54
55
# File 'lib/carray/methods/string_format.rb', line 53

def format (fmt, *argv)
  CArray.format(fmt, self, *argv)
end

#from_bit_string(bstr, nb) ⇒ self

Sets self by unpacking bstr as a packed-bit byte string with nb bits per element.

Parameters:

  • bstr (String)

    packed byte string.

  • nb (Integer)

    bits per element.

Returns:

  • (self)


24
25
26
27
28
29
30
# File 'lib/carray/methods/bit_string.rb', line 24

def from_bit_string (bstr, nb)
  hex = CArray.uint8(bstr.length).load_binary(bstr)
  hex.bits[] = hex.bits[nil,[-1..0]]
  bits = hex.bits.flatten
  self.bits[false,[(nb-1)..0]][nil].paste([0], bits)
  return self
end

#gather_nd(indices) ⇒ CArray

Returns elements (or sub-arrays) gathered from self at the N-D coordinates given by indices.

indices accepts two equivalent forms:

  • stacked — a single CArray whose last axis enumerates a K-dimensional coordinate tuple into the first K axes of self.
  • per-axis — an Array of K coordinate CArrays (one per consumed axis). The per-axis arrays are broadcast together (via broadcast) and stacked along a new trailing axis, so gather_nd([i, j]) == gather_nd(CArray.stack([i, j], axis: -1)). An Integer scalar is accepted for a constant axis and broadcast to the common shape. Entries must be CArray or Integer: Ruby Array literals are rejected (wrap them with CA_INT64(...) yourself), keeping this a copy-free gather path.

The remaining self.shape[K..-1] axes (called rest) are carried through:

self.shape    = (D0, ..., D_{K-1}, *rest)
indices.shape = (*outer, K)          # stacked form
result.shape  = (*outer, *rest)

In the fully-degenerate case where both outer and rest are empty (a single full coordinate via 1-D indices of shape (K,) with K == ndim), the result is a 1-element (1,) CArray, following CArray's scalar model (CScalar carries shape [1]), not a 0-dim array.

The result is a fresh materialised CArray. Negative indices on each coordinate axis follow CArray's standard wrap rule (-1 == last); out-of-range indices raise. Duplicate coordinates in indices are fine on gather: the same value is picked multiple times. See #put_nd for the duplicate-write story.

Parameters:

  • indices (CArray, Array<CArray, Integer>)

    stacked integer CArray with ndim >= 1 and last axis size K in [1, ndim], or an Array of K per-axis coordinate CArrays (Integer scalars allowed per axis).

Returns:

  • (CArray)

    materialised result with shape outer + rest.

Raises:

  • (ArgumentError)

    when indices is neither a CArray nor an Array, is 0-dim, is non-integer, or has a last-axis size outside [1, ndim].

  • (IndexError)

    when a coordinate is out of range on any axis.



63
64
65
66
67
68
69
# File 'lib/carray/methods/gather_nd.rb', line 63

def gather_nd (indices)
  flat_addr, outer, rest = gather_nd_flat_addr(indices, "gather_nd")
  out_shape = outer + rest
  # flatten + 1-D fancy indexing -> CAMapping view -> materialise via .copy.
  result = self.flatten[flat_addr].copy
  out_shape.empty? ? result : result.reshape(*out_shape)
end

#group_by_category(cat) ⇒ CACategoricalIterator

Returns a CACategoricalIterator that reduces self (the payload) per category of cat. Requires self.elements == cat.elements.

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when element counts differ.



1016
1017
1018
# File 'lib/carray/categorical_iterator.rb', line 1016

def group_by_category (cat)
  CACategoricalIterator.new(self, cat)
end

#group_by_runCACategoricalIterator

Segments self into maximal runs of consecutive non-masked cells and returns a CACategoricalIterator that reduces each run as one category, ordered by position. The run boundary is the mask: a masked cell belongs to no run and breaks any run across it. State what separates runs (the "background") by masking before the call — e.g. ca.mask_where(:le, 0) makes non-positive cells background without mutating ca. A series with no present cell yields zero groups rather than raising. 1-D only.

prec = CA_DOUBLE([1,2,2,2,0,0,0,2,1,2,0,0,0,3,2,3,2,1,0,0,0])
grp  = prec.mask_where(:le, 0).group_by_run
grp.sum     # => [7.0, 5.0, 11.0]   per-run accumulation
grp.count   # => [4, 3, 5]          per-run length
grp.each { |members| ... }          # each run as a CArray

The run categories are labelled by their 0-based run index, so grp.labels is [0, 1, ...] in position order.

Returns:

Raises:

  • (RuntimeError)

    when self is not 1-D.



1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
# File 'lib/carray/categorical_iterator.rb', line 1042

def group_by_run
  raise "group_by_run: 1-D only (got #{ndim}-D)" unless ndim == 1
  if elements == 0
    code = CArray.int64(0)
  else
    present = is_not_masked
    edge    = present & present.shift(1).not   # rising edge = run start
    # feed cumsum via a zero-copy int8 reinterpret of the 1-byte booleans
    # rather than widening to int64; cumsum promotes to float64, so the
    # running count never overflows int8.
    code    = edge.refer(:int8).cumsum.int64 - 1   # 0-based run index per cell
    code[present.not] = UNDEF                  # masked cells join no run
  end
  # categorize turns the dense run indices into the run categories: it derives
  # the label vocabulary and folds an all-masked (dry) series to zero groups
  # on its own, so no explicit run count is needed here. code is monotonic (a
  # cumsum), so categorize's first-appearance order is already run order and
  # sort_labels would be a no-op.
  group_by_category(code.categorize)
end

#has_attr?Boolean #has_attr?(key) ⇒ Boolean

Overloads:

  • #has_attr?Boolean

    Returns whether self (or any parent view) has any attribute set.

    Returns:

  • #has_attr?(key) ⇒ Boolean

    Returns whether self (or any parent view) has attribute key set.

    Parameters:

    • key (Symbol, String)

      attribute key.

    Returns:



87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/carray/attribute.rb', line 87

def has_attr? (key = nil)
  if key.nil?
    attr_each_chain do |h|
      return true unless h.empty?
    end
    false
  else
    k = attr_normalize_key(key)
    attr_each_chain do |h|
      return true if h.key?(k)
    end
    false
  end
end

#histogram(edges:, axis: [-2, -1], include_max: false, weights: nil) ⇒ Histogram

Returns an M-D joint Histogram built from self with shape fiber_shape + (A, M), where M == edges.size.

Parameters:

  • edges (Array<CArray, Array<Numeric>>)

    one edges array per dimension.

  • axis (Array(Integer, Integer)) (defaults to: [-2, -1])

    [sample, channel] axis pair.

  • include_max (Boolean, Array<Boolean>) (defaults to: false)

    fold-max flag, per dimension.

  • weights (CArray, nil) (defaults to: nil)

    optional per-sample weights.

Returns:

Raises:

  • (ArgumentError)

    when edges is not an Array.

Raises:

  • (ArgumentError)


491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/carray/histogram.rb', line 491

def histogram (edges:, axis: [-2, -1], include_max: false, weights: nil)
  raise ArgumentError, "edges must be an Array of edges arrays" unless edges.is_a?(Array)
  arr = self
  sample_ax  = normalize_axis(axis[0], "histogram sample axis")
  channel_ax = normalize_axis(axis[1], "histogram channel axis")
  fiber_shape = arr.shape.dup
  [sample_ax, channel_ax].sort.reverse.each { |p| fiber_shape.delete_at(p) }

  # Weighted counts are float64-only (the fused scatter kernel requires
  # float64 weights and float64 counts), so the dtype is fixed here rather
  # than derived from the weights' own dtype.
  weights_dtype = (:float64 if weights)

  h = Histogram.send(:new,
                    edges: edges,
                    fiber_shape: fiber_shape,
                    include_max: include_max,
                    weights_dtype: weights_dtype)
  h.add(arr, axis: axis, weights: weights)
  h
end

#histogram1d(edges:, axis: -1, include_max: false, weights: nil) ⇒ Histogram

Returns a 1-D Histogram built from self with shape fiber_shape + (A,), where A is the sample axis of length picked by axis.

Parameters:

  • edges (CArray, Array<Numeric>)

    1-D ascending bin edges.

  • axis (Integer) (defaults to: -1)

    sample axis.

  • include_max (Boolean) (defaults to: false)

    fold last-edge equality into the last bin.

  • weights (CArray, nil) (defaults to: nil)

    optional per-sample weights.

Returns:



449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/carray/histogram.rb', line 449

def histogram1d (edges:, axis: -1, include_max: false, weights: nil)
  ax = normalize_axis(axis, "histogram1d")

  new_shape = shape + [1]
  arr_with_channel = reshape(*new_shape)
  # `include_max` passes straight through: the Histogram constructor
  # normalizes a scalar bool to per-dim, and raises on a wrong-length Array
  # (= same path as histogram2d, no M=1 special-casing here).
  arr_with_channel.histogram(edges: [edges],
                             axis: [ax, new_shape.size - 1],
                             include_max: include_max,
                             weights: weights)
end

#histogram2d(edges:, axis: [-2, -1], include_max: false, weights: nil) ⇒ Histogram

Returns a 2-D joint Histogram built from self with shape fiber_shape + (A, 2).

Parameters:

  • edges (Array<CArray, Array<Numeric>>)

    two edges arrays.

  • axis (Array(Integer, Integer)) (defaults to: [-2, -1])

    [sample, channel] axis pair.

  • include_max (Boolean, Array<Boolean>) (defaults to: false)

    fold-max flag, per dimension.

  • weights (CArray, nil) (defaults to: nil)

    optional per-sample weights.

Returns:

Raises:

  • (ArgumentError)

    when edges is not a length-2 Array.

Raises:

  • (ArgumentError)


474
475
476
477
# File 'lib/carray/histogram.rb', line 474

def histogram2d (edges:, axis: [-2, -1], include_max: false, weights: nil)
  raise ArgumentError, "edges must be a list of 2" unless edges.is_a?(Array) && edges.size == 2
  histogram(edges: edges, axis: axis, include_max: include_max, weights: weights)
end

#imagCArray

Returns the imaginary part of self. For a complex array the result is a mutable CAField view of the imaginary slot; writing to it updates self in place. For a real array the result is a fresh independent CArray filled with 0.

Returns:



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/carray/complex.rb', line 91

def imag
  if not @__imag__
    if complex?
      @__imag__ = case data_type
                  when CA_CMPLX64
                    field(4, CA_FLOAT32)
                  when CA_CMPLX128
                    field(8, CA_FLOAT64)
                  end
    else
      @__imag__ = self.template { 0 }
    end
  end
  return @__imag__
end

#imag=(val) ⇒ Object

Sets the imaginary-part slot to val (complex arrays only).

Parameters:

  • val (CArray, Numeric)

    value to broadcast.

Returns:

Raises:

  • (RuntimeError)

    when self is not a complex array.



112
113
114
115
116
117
118
# File 'lib/carray/complex.rb', line 112

def imag= (val)
  if complex?
    imag[] = val
  else
    raise "not a complex array"
  end
end

#index(axis: 0) ⇒ CArray

Returns a writable int32 CArray holding the coordinate ramp [0, 1, ..., shape[axis] - 1] along axis, in an open broadcast shape: size shape[axis] on that axis and 1 on every other axis (e.g. for (d0, d1, d2), index(axis: 1) returns (1, d1, 1)).

The result broadcasts against self in element-wise ops without materialising the full shape. For the dense full-shape grid use index(axis: k).broadcast_to(*shape) or CArray.meshgrid.

Parameters:

  • axis (Integer) (defaults to: 0)

    axis to vary (negative counts from the end).

Returns:

  • (CArray)

    writable int32 CArray; size 1 on every axis except axis, which has size shape[axis].



19
20
21
22
23
24
# File 'lib/carray/methods/index.rb', line 19

def index (axis: 0)
  k = normalize_axis(axis, "index")
  oshape = Array.new(ndim, 1)
  oshape[k] = shape[k]
  CArray.int32(*oshape).seq!
end

#indicesArray<CArray> #indices({ |*ramps| ... }) {|*ramps| ... } ⇒ Object

Overloads:

  • #indicesArray<CArray>

    Returns an Array of +ndim+ coordinate ramps, one per axis, each in the open broadcast shape of #index.

    Returns:

    • (Array<CArray>)

      one open coordinate ramp per axis.

  • #indices({ |*ramps| ... }) {|*ramps| ... } ⇒ Object

    Yields the +ndim+ open coordinate ramps as splat arguments.

    Yields:

    • (*ramps)

      the per-axis open coordinate ramps.

    Returns:

    • (Object)

      the block's return value.



34
35
36
37
# File 'lib/carray/methods/index.rb', line 34

def indices
  list = (0...ndim).map { |k| index(axis: k) }
  block_given? ? yield(*list) : list
end

#insert_axis(*positions, repeat: nil) ⇒ CArray

Returns a view of self with one or more new axes inserted, optionally repeating along them.

Each entry of positions names the source axis the new axis goes before. ndim (one past the last axis) appends at the end; negative positions count from the end. Repeating the same position inserts several axes before that axis, in argument order. Positions are in the source frame, so they do not shift as other axes are inserted (e.g. insert_axis(0, 1, 2) puts one axis before each of the first three source axes).

Each inserted axis takes one of three forms, chosen by its repeat value: 1 (or nil) for a plain size-1 axis, an Integer N > 1 for a read-only bound repeat view, or :* for an unbound repeat that binds on assignment. repeat is either a single value applied to every inserted axis, or an Array giving one value per position.

The everyday way to add an axis is the :_ / :* indexer when the shape is known at the call site; insert_axis is for library code that builds the axis list programmatically.

Examples:

a = CArray.int32(3, 4).seq
a.insert_axis(0)                       # shape (1, 3, 4)
a.insert_axis(1, repeat: 5)            # shape (3, 5, 4)
a.insert_axis(0, 1, repeat: [:*, 3])   # mixed unbound + bound

Parameters:

  • positions (Array<Integer>)

    source-frame positions of the new axes.

  • repeat (Integer, Symbol, Array, nil) (defaults to: nil)

    repeat spec applied to each inserted axis.

Returns:

  • (CArray)

    view with the new axes inserted.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/carray/basics.rb', line 54

def insert_axis (*positions, repeat: nil)
  flat = positions.flatten
  if flat.empty?
    raise ArgumentError, "insert_axis: at least one position is required"
  end

  # No repeat: -> plain size-1 insertion.  The source-frame C primitive
  # handles normalization, range check and multiplicity directly.
  return __insert_axis_size1__(*flat) if repeat.nil?

  # Source frame: each position names the source axis the new axis goes
  # before.  Gaps live in [0, ndim] (ndim = append at end); negatives count
  # from the end gap.  Duplicates are allowed (several axes before one
  # source axis), kept in argument order.
  gaps = flat.map { |p| CArray.normalize_axis(p, ndim + 1, "insert_axis") }

  # One repeat value per position, in argument order.
  reps =
    case repeat
    when Array
      unless repeat.length == flat.length
        raise ArgumentError,
          "insert_axis: repeat array length (#{repeat.length}) " \
          "must match number of positions (#{flat.length})"
      end
      repeat
    else
      Array.new(flat.length, repeat)
    end

  # Validate each value.  A positive Integer or :* only; nil is not a
  # valid per-axis repeat.
  reps.each do |r|
    case r
    when Integer
      raise ArgumentError, "insert_axis: repeat count must be >= 1" if r < 1
    when :*
      # ok
    else
      raise ArgumentError,
        "insert_axis: repeat must be a positive Integer or :*, got #{r.inspect}"
    end
  end

  # Final output layout: stable order by (gap, argument index) keeps
  # same-gap axes in argument order; the k-th inserted axis lands at output
  # position gap + k.  This output position is only used to drive the
  # output-shaped view constructors (broadcast_to / unbound_repeat); the
  # actual insertion always goes through the source-frame primitive below.
  order = (0...flat.length).sort_by { |i| [gaps[i], i] }
  final = {}
  order.each_with_index { |i, k| final[i] = gaps[i] + k }

  unbound_args  = order.select { |i| reps[i] == :* }
  concrete_args = order.reject { |i| reps[i] == :* }   # in output order

  # Stage 1: insert the concrete (size-1 / bound) axes by their source
  # gaps, then grow the bound ones with broadcast_to.
  inter = self
  unless concrete_args.empty?
    inter = __insert_axis_size1__(*concrete_args.map { |i| gaps[i] })
    if concrete_args.any? { |i| reps[i].is_a?(Integer) && reps[i] > 1 }
      shp = inter.shape
      concrete_args.each do |i|
        r = reps[i]
        next unless r.is_a?(Integer) && r > 1
        # intermediate position = final position minus unbound axes before it
        shp[final[i] - unbound_args.count { |u| final[u] < final[i] }] = r
      end
      inter = inter.broadcast_to(*shp)
    end
  end

  return inter if unbound_args.empty?

  # Stage 2: add the unbound axes over the final ndim (`:*` at unbound
  # positions, nil consumes one stage-1 axis in order).
  pattern = Array.new(ndim + flat.length, nil)
  unbound_args.each { |i| pattern[final[i]] = :* }
  inter.unbound_repeat(*pattern)
end

#insert_block(offset, bsize, &block) ⇒ CArray

Returns a new CArray obtained by inserting a block of size bsize (per axis) at offset, growing the array.

Per axis, offset accepts 0..shape[i] (equal to shape[i] appends at the end) and a negative value counts from the end; bsize[i] must be non-negative. Inserted cells are filled by the block's return value, or left at the type's default when no block is given. Preserves fixlen bytes and Face identity (built on the storage layout, re-wrapped via face_lift). The offset array is not mutated.

Parameters:

  • offset (Array<Integer>)

    insertion offset per axis.

  • bsize (Array<Integer>)

    block size per axis.

Yield Returns:

  • (Object)

    fill value for the inserted cells.

Returns:

Raises:

  • (ArgumentError)

    on ndim mismatch or out-of-range offset / size.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/carray/methods/insert_block.rb', line 24

def insert_block (offset, bsize, &block)
  if offset.size != ndim or bsize.size != ndim
    raise ArgumentError, "ndim mismatch"
  end
  offset = offset.dup           # normalize without mutating the caller's array
  newdim = shape
  grids = shape.map{|d| CArray.int32(d) }
  ndim.times do |i|
    offset[i] += shape[i] if offset[i] < 0
    if offset[i] < 0 or offset[i] > shape[i] or bsize[i] < 0
      raise ArgumentError, "invalid offset or size at axis #{i}"
    end
    if bsize[i] > 0
      newdim[i] += bsize[i]
    end
    grids[i][0...offset[i]].seq! if offset[i] > 0
    # offset == dim (append) leaves nothing on the upper side to shift.
    grids[i][offset[i]..-1].seq!(offset[i]+bsize[i]) if offset[i] < shape[i]
  end
  # Build at the storage layout (preserving bytes for fixlen / Face),
  # then re-wrap as the same Face.
  face_parent = self.face? ? self : nil
  src = self
  src = src.parent while src.face?
  dt    = src.data_type
  bytes = (dt == :fixlen) ? src.bytes : nil
  out = CArray.new(dt, newdim, bytes: bytes)
  if block_given?
    sel = out.true
    sel[*grids] = 0
    out[sel] = block.call
  end
  out[*grids] = src
  out = out.face_lift(face_parent) if face_parent
  return out
end

#inspectString

Returns a human-readable description of self including class, data_type, shape, element and memory summaries, mask count, and a truncated data preview.

Returns:

  • (String)


224
225
226
# File 'lib/carray/inspect.rb', line 224

def inspect
  return CArray::Inspector.new(self).inspect_string
end

#intersection(other, sort: false) ⇒ CArray

Returns a 1-D CArray of the distinct values appearing in both self and other, in self's first-appearance order.

Value-based, sharing the distinctness of the discovery family (see #is_in); self and other are promoted to a common dtype. Masked cells of either array do not participate. The result is always flat, like #unique, because the distinct values of a fiber vary in number.

Parameters:

  • other (CArray, Array, Range)

    promoted with self.

  • sort (Boolean) (defaults to: false)

    when true, return the values sorted ascending instead of in first-appearance order.

Returns:

  • (CArray)

    1-D CArray of the common distinct values.



61
62
63
64
65
# File 'lib/carray/methods/is_in.rb', line 61

def intersection (other, sort: false)
  a, b = promote_value_set(other)
  r = a.__send__(:__intersection__, b)
  sort ? r.sort : r
end

#is_in(values) ⇒ CArray

Returns a boolean CArray of the same shape as self, true at each cell whose value appears in the set values.

values is treated as a set, not as an operand to broadcast: it may be any shape (or an Array / Range) and is flattened to a single seen-set, so its shape need not match self. Only one argument is accepted; to test a few immediate values pass an Array (a.is_in([0, -1])).

When self and values have different numeric dtypes they are promoted to a common type first (the same promotion binops use, result_type), so membership is value-correct across dtypes (e.g. an int cell equals a float set element of the same value, and a fractional set element never truncates onto an int cell). Genuinely incompatible dtypes (e.g. numeric vs fixlen) raise.

Membership is value-based and shares the distinctness of the value-hash discovery family (#unique / #value_counts): numeric follows == with all NaN collapsed to one value and -0.0 == +0.0; CA_OBJECT follows Ruby hash / eql? with Float NaN collapsed; CA_FIXLEN follows byte equality.

Masked cells of values do not enter the set. Masked cells of self stay masked in the result (membership is unknown), so is_in propagates self's mask like an element-wise comparison.

For a per-fiber "does this fiber contain any of these values" reduction, compose with #any: a.is_in(values).any(axis: k).

Between two time arrays the question is about instants, not ticks: values is reconciled into self's unit first, so a :D array and an :h array match on the instants they share. The same holds for the set operations below, whose results come back as self's own type.

Parameters:

  • values (CArray, Array, Range)

    the set to test membership against. Promoted with self to a common dtype.

Returns:

  • (CArray)

    boolean CArray of the same shape as self.



42
43
44
45
# File 'lib/carray/methods/is_in.rb', line 42

def is_in (values)
  a, b = promote_value_set(values)
  a.__send__(:__is_in__, b)
end

#is_mode(axis: nil) ⇒ CArray

Returns a shape-preserving boolean CArray, true at every cell that holds a modal value — a value whose occurrence count equals the maximum count. This is the first-class primitive of the mode family: rather than returning the mode value (whose count is data-dependent when there are ties), it marks every occurrence of every most-frequent value, so ties are never silently broken.

CA_INT32([1, 1, 2, 2, 3]).is_mode  # => [1, 1, 1, 1, 0]  (1 and 2 tie)

With axis: nil the frequency is over the whole array; with axis: k it is per fiber along axis k, independently. The result is always the input shape, so — unlike returning the mode value — the per-axis form has no ragged-length problem. Select the modal cells with a[a.is_mode] / a[a.is_mode(axis: k)] (the #mask_duplicates idiom); reduce further with .min / .unique.

Mode is only meaningful for discrete or binned data: raw float is almost all unique, so is_mode would mark just the single lowest cell (count 1). Bin first (bin / histogram / categorize).

Masked cells do not participate and are marked false. An empty or all-masked fiber marks every cell false (mode has no identity; it never raises). Numeric distinctness follows the discovery family (all NaN collapse to one value, -0.0 == +0.0); CA_OBJECT / CA_FIXLEN follow Ruby eql? / hash.

Parameters:

  • axis (Integer, nil) (defaults to: nil)

    axis to take the mode along; nil uses the whole array.

Returns:

  • (CArray)

    boolean CArray of self.shape.



35
36
37
38
39
40
41
42
43
44
# File 'lib/carray/methods/mode.rb', line 35

def is_mode (axis: nil)
  # Per-fiber two-pass frequency table (C __is_mode__), one lane per dtype
  # family (numeric widen / NaN collapse, object rb_hash + rb_eql, fixlen
  # byte-hash + memcmp). Ties are all marked; masked cells stay false.
  if axis.nil?
    flatten.send(:__is_mode__, 0).reshape(*shape)
  else
    __is_mode__(normalize_axis(axis, "is_mode"))
  end
end

#is_realCArray?

Returns an element-wise boolean CArray marking cells whose imaginary part is zero (all-true for real numeric arrays, nil for non-numeric arrays).

Returns:



140
141
142
143
144
145
146
147
148
# File 'lib/carray/complex.rb', line 140

def is_real
  if complex?
    imag.eq(0)
  elsif numeric?
    self.true
  else
    nil
  end
end

#join(sep = nil) ⇒ Object #join(sep = "", axis:, keep_axis: false) ⇒ CArray, String

Note:

The 2.x multi-separator form a.join("\n", ",") was removed in 3.0; use the axis form and chain, e.g. a.join(",", axis: 1).join("\n").

Examples:

Flat form

a = CArray.object(3, 3).seq("a", :succ)
a.join            # => "abcdefghi"
a.join(",")       # => "a,b,c,d,e,f,g,h,i"

Per-axis form

a = CArray.int32(3, 3).seq
a.join(" ", axis: 1)               # → CArray["0 1 2", "3 4 5", "6 7 8"]
a.join(" ", axis: 1).join("\n")    # => "0 1 2\n3 4 5\n6 7 8"

Overloads:

  • #join(sep = nil) ⇒ Object

    Flat form. Stringifies and concatenates every element of self (as if to_a.flatten.join(sep)). Returns a String.

  • #join(sep = "", axis:, keep_axis: false) ⇒ CArray, String

    Per-axis form. Reduces axis into strings, one per fiber along that axis, and returns the result as a CArray with axis removed (or set to 1 when keep_axis: true). Composable — call join again on the result to collapse another axis or fold to a String.

    For a 1-D self, the axis form fully reduces and returns the String directly (matching the flat form and the reduction convention).

    Parameters:

    • sep (String) (defaults to: "")

      separator between elements along the axis.

    • axis (Integer)

      axis to reduce (negative allowed).

    • keep_axis (Boolean) (defaults to: false)

      keep the reduced axis as length 1.

    Returns:

    • (CArray, String)

      a CArray of strings, or a String when self is 1-D.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/carray/methods/join.rb', line 36

def join (*argv, axis: nil, keep_axis: false)
  if argv.size > 1
    raise ArgumentError,
          "join accepts at most one positional separator " \
          "(the 2.x multi-separator form was removed in 3.0; " \
          "use axis: for per-axis join and chain)"
  end
  sep = argv.first  # nil or String

  if axis.nil?
    return sep.nil? ? to_a.join : to_a.join(sep)
  end

  ax = Integer(axis)
  ax += ndim if ax < 0
  if ax < 0 || ax >= ndim
    raise ArgumentError,
          "axis #{axis.inspect} out of range for ndim=#{ndim}"
  end

  sep_str = sep || ""

  # Bring `ax` to the innermost position so we can iterate fibers as
  # rows of a 2-D reshape.  transpose returns a view; reshape may
  # materialize on non-contig, which is fine for this formatting op.
  if ax == ndim - 1
    t = self
  else
    order = (0...ndim).to_a
    order << order.delete_at(ax)
    t = transpose(*order)
  end
  inner = t.shape[-1]
  outer_n = t.elements / inner  # 1 when ndim == 1
  flat = t.reshape(outer_n, inner)

  strings = Array.new(outer_n) { |i| flat[i, nil].to_a.join(sep_str) }

  if ndim == 1
    # Full reduction: return the String directly, or a length-1
    # CArray when keep_axis was requested.
    return keep_axis ? CA_OBJECT([strings.first]) : strings.first
  end

  result_shape = shape.dup
  if keep_axis
    result_shape[ax] = 1
  else
    result_shape.delete_at(ax)
  end

  CA_OBJECT(strings).reshape(*result_shape)
end

#locate_addr(ref) ⇒ Object

User-facing YARD docs for #locate_addr and #locate_nearest_addr live in yard-stubs/carray_order.rb (grouped with the search family).



6
7
8
9
10
11
12
13
14
15
16
17
# File 'lib/carray/methods/locate_addr.rb', line 6

def locate_addr (ref)
  ref = ref.to_ca unless ref.is_a?(CArray)
  # Put self and ref in a common lane via the single-source promotion rule
  # (CArray.result_type), so a fractional query against an int ref is compared
  # at the promoted type instead of truncating (1.5 no longer matches 1).
  # to_type is elementwise and order-preserving, so the addresses stay valid
  # indices into ref. result_type raises for cross-family input.
  t = CArray.result_type(self, ref)
  q = (data_type     == t) ? self : to_type(t)
  r = (ref.data_type == t) ? ref  : ref.to_type(t)
  q.send(:__locate_addr__, r)
end

#locate_nearest_addr(ref, direction: :round, tolerance: nil) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/carray/methods/locate_addr.rb', line 19

def locate_nearest_addr (ref, direction: :round, tolerance: nil)
  unless [:round, :floor, :ceil].include?(direction)
    raise ArgumentError,
          "locate_nearest_addr: direction must be :round / :floor / " \
          ":ceil (got #{direction.inspect})"
  end
  ri = ref.sort_addr
  rs = ref[ri]
  sec = rs.linear_section(self)
  unless sec.is_a?(CArray)
    # A single-element (scalar-like) self makes linear_section collapse to
    # its scalar-query path, which returns a bare Float (or nil when out of
    # range) instead of a CArray.  Rebuild a self-shaped float64 CArray so
    # the mask_invalid -> direction -> project pipeline stays array-valued
    # and the returned addr array matches self's shape.
    fill = CArray.float64(*shape)
    fill[] = sec.nil? ? UNDEF : sec
    sec = fill
  end
  si = sec.mask_invalid.send(direction).int64
  idx = ri.project(si)
  if tolerance
    dist = (ref.project(idx) - self).abs
    idx[dist > tolerance] = UNDEF
  end
  idx
end

#lookup(table, fill_value = nil, lfill: nil, ufill: nil) ⇒ CArray

Returns values gathered from table at the indices given by self. Equivalent to table.project(self, lfill, ufill) with the receiver / first argument swapped so the index reads as the subject. fill_value is sugar for symmetric dual-fill; lfill / ufill override per side (UNDEF or nil masks that end), following the project vocabulary.

Parameters:

  • table (CArray)

    value table indexed by self.

  • fill_value (Object, nil) (defaults to: nil)

    symmetric fill for below- and above-range indices.

  • lfill (Object, nil) (defaults to: nil)

    override for below-range fill.

  • ufill (Object, nil) (defaults to: nil)

    override for above-range fill.

Returns:

  • (CArray)

    gathered values with the shape of self.



231
232
233
234
235
# File 'lib/carray/basics.rb', line 231

def lookup(table, fill_value=nil, lfill: nil, ufill: nil)
  lfill = fill_value if lfill.nil?
  ufill = fill_value if ufill.nil?
  table.project(self, lfill, ufill)
end

#marshal_dumpArray

Returns the Marshal payload for self. Virtual / wrapped arrays are copied first so the payload always describes an owning array.

Returns:



518
519
520
521
522
523
524
525
526
527
528
# File 'lib/carray/serialize.rb', line 518

def marshal_dump ()
  target = (self.class != CArray and self.class != CScalar) ? self.copy : self
  if target.data_type == :object
    ["object",
     target.shape,
     target.value.to_a,
     (target.has_mask? ? target.mask.to_a : nil)]
  else
    ["portable", CArray.dump(target)]
  end
end

#marshal_load(data) ⇒ void

This method returns an undefined value.

Reconstitutes self from a Marshal payload produced by #marshal_dump.

Parameters:

  • data (Array)

    Marshal payload.



535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
# File 'lib/carray/serialize.rb', line 535

def marshal_load (data)
  tag, *rest = data
  case tag
  when "object"
    shape, values, mask = rest
    ca = CArray.object(*shape)
    ca[] = values
    if mask
      ca.mask = 0
      ca.mask[] = mask
    end
    initialize_copy(ca)
  when "portable"
    ca = CArray.load(StringIO.new(rest[0]))
    initialize_copy(ca)
  else
    raise TypeError, "unrecognised CArray Marshal payload"
  end
end

#mask_duplicates(axis: nil) ⇒ CArray

Returns a shape-preserving copy of self with the mask set at every cell whose value duplicates an earlier-seen one; the first occurrence is kept.

With axis: nil duplicates are detected in flatten (row-major) order across the whole array. With axis: k duplicates are detected per-fiber along axis k, independently for each fiber. Marking duplicates (not compressing) is what makes the per-axis form expressible: fibers may hold different numbers of distinct values, so a compressed result would be ragged.

Distinctness matches the value-hash discovery family. Numeric: == with all NaN collapsed to one value (so the second and later NaN are duplicates) and -0.0 == +0.0. CA_OBJECT / CA_FIXLEN: Ruby eql? / hash (distinct NaN objects stay distinct). Masked input cells stay masked and do not participate in duplicate judging. Both axis: nil and axis: k work.

Parameters:

  • axis (Integer, nil) (defaults to: nil)

    axis to detect duplicates along; nil uses flatten order.

Returns:

  • (CArray)

    shape-preserving copy of self with duplicates masked.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/carray/methods/mask_duplicates.rb', line 26

def mask_duplicates (axis: nil)
  dup =
    if axis.nil?
      # One seen-set over the flattened array, then restore shape.
      flatten.send(:__mask_duplicates__, 0).reshape(*shape)
    else
      # Per-fiber single-pass seen-set hash (C __mask_duplicates__): one lane
      # per dtype family (integer widen, float bitwise key with NaN collapse,
      # object rb_hash + rb_eql, fixlen byte-hash + memcmp, boolean via the
      # uint8 lane). O(distinct) memory, no sort/gather/scatter buffers.
      __mask_duplicates__(normalize_axis(axis, "mask_duplicates"))
    end
  mask_where(dup)
end

#meld(*others, axis: 0) ⇒ CAMeld

Returns a CAMeld view of [self, *others] welded along axis. Instance form of meld; non-destructive, see the class method for full semantics.

Parameters:

  • others (Array<CArray>)

    additional pieces.

  • axis (Integer) (defaults to: 0)

    existing axis to extend.

Returns:

Raises:

  • (ArgumentError)

    when no others are given.

Raises:

  • (ArgumentError)


123
124
125
126
# File 'lib/carray/stack.rb', line 123

def meld (*others, axis: 0)
  raise ArgumentError, "meld: at least one other array required" if others.empty?
  CArray.meld(self, *others, axis: axis)
end

#mode(axis: nil) ⇒ CArray+

Returns the distinct modal values — the most frequent value(s), ascending. All values that tie for the highest count are returned (matching pandas Series.mode), because #is_mode does not break ties; mode is the value-form consumer of that primitive, read straight from the frequency table.

With axis: nil the result is a 1-D CArray of the distinct modal values over the whole array (empty when all-masked).

With axis: k the per-fiber mode counts are ragged, so — like per-axis quantile — the result is an Array of reduced CArrays. Element j holds each fiber's j-th smallest modal value, masked where a fiber has fewer than j + 1 modes; the Array length is the widest fiber's mode count. So mode(axis: k)[0] is the smallest mode of each fiber (a plain reduced CArray). To get the rectangular mask-padded form, stack them: CArray.stack(a.mode(axis: k), axis: k). An all-masked array yields an empty Array.

Only meaningful for discrete / binned data: raw float is almost all unique, so every value is modal and the per-axis Array grows to the fiber length. Bin first. Same NaN / mask semantics as #is_mode.

Parameters:

  • axis (Integer, nil) (defaults to: nil)

    axis to take the mode along; nil uses the whole array.

Returns:

  • (CArray, Array<CArray>)

    1-D CArray (flat) or an Array of reduced CArrays, one per mode rank (per-axis).



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/carray/methods/mode.rb', line 73

def mode (axis: nil)
  return __mode_flat if axis.nil?
  k = normalize_axis(axis, "mode")

  # Numeric: the C frequency-table kernel emits the ragged Array<CArray>
  # directly (reduced CArrays, self.shape with axis k dropped). A 1-D input
  # reduces to length-1 CArrays, unwrapped to scalars like flat quantile.
  unless data_type == CA_OBJECT || data_type == CA_FIXLEN
    cols = __mode_axis__(k)
    return ndim == 1 ? cols.map { |col| col[0] } : cols
  end

  # Object / fixlen (rare): per-fiber Ruby path, reusing the flat mode as the
  # single source of what counts as a mode. Move axis k to the innermost
  # position and fold the rest to one outer axis, so each row is a fiber.
  perm = (0...ndim).to_a
  perm.delete(k)
  perm << k
  a2    = (ndim == 1) ? self : transpose(*perm).copy   # (outer..., L)
  outer = a2.shape[0...-1]
  m     = outer.empty? ? 1 : outer.inject(:*)
  flat2 = a2.reshape(m, a2.shape[-1])
  lists = Array.new(m) { |r| flat2[r, nil].__send__(:__mode_flat).to_a }

  # K = widest fiber's mode count. Emit K reduced CArrays (like quantile's
  # per-axis Array<CArray>): slot j holds each fiber's j-th smallest mode,
  # masked where a fiber has fewer than j+1 modes. Stack them to get the
  # rectangular mask-padded form: CArray.stack(result, axis: k).
  kk = lists.map(&:size).max || 0
  (0...kk).map do |j|
    # Take the column shape from self rather than building it from data_type:
    # it carries the element width a fixlen array needs, and it keeps a Face
    # (a time array), whose cells then accept the surface values in `lists`.
    col = flat2[nil, 0].copy
    col[] = UNDEF
    m.times { |r| col[r] = lists[r][j] if j < lists[r].size }
    outer.empty? ? col[0] : col.reshape(*outer)
  end
end

#none(skip_masked: true, **opts) ⇒ Boolean, CArray

Whether no cell is true.

With skip_masked: true (the default) masked cells are simply ignored and the result is always true / false. With skip_masked: false the fold is three-valued: the result is UNDEF when a masked cell could change it, matching the element-wise Kleene semantics of | / &.

Parameters:

  • skip_masked (Boolean) (defaults to: true)

    ignore masked cells, or fold them three-valued.

  • opts (Hash)

    forwarded to the underlying reduction (axis:, keep_axis:, ...).

Returns:

  • (Boolean, CArray)

    a scalar, or an array when an axis is given.



70
71
72
73
74
75
# File 'lib/carray/boolean_reduce.rb', line 70

def none (skip_masked: true, **opts)
  return __none_skipna__(**opts) if skip_masked
  # none = not any (Kleene): not(true)=false, not(false)=true, not(UNDEF)=UNDEF
  r = __kleene_fold(:any, opts)
  r.is_a?(CArray) ? r.not : (r.equal?(UNDEF) ? UNDEF : !r)
end

#nunique(axis: nil, keep_axis: false) ⇒ Integer, CArray

Counts the distinct values of self. This is the scalar-reduction member of the value-hash discovery family (#unique, #mask_duplicates, #value_counts): where #unique compresses and #value_counts tabulates, nunique just counts.

With axis: nil (default) it counts distinct values across the whole array and returns an Integer. With axis: k it counts per fiber along axis k, returning a reduced CA_INT64 CArray (shape = self.shape with axis k removed; keep_axis: true keeps it as a length-1 axis). The shape rule matches other per-axis reductions such as sum(axis:).

The distinct count has identity 0: an empty array, an all-masked fiber, or a zero-length axis counts 0 (not UNDEF) — an empty set has zero distinct values. Masked cells do not participate.

Numeric distinctness follows == with the family's two float special cases: all NaN collapse to a single value and -0.0 / +0.0 are the same value. For CA_OBJECT / CA_FIXLEN distinctness follows Ruby eql? / hash (which, unlike numeric, does not collapse distinct NaN objects).

Parameters:

  • axis (Integer, nil) (defaults to: nil)

    axis to count along; nil counts over the whole array.

  • keep_axis (Boolean) (defaults to: false)

    when axis is given, keep the reduced axis as a length-1 axis instead of dropping it.

Returns:

  • (Integer, CArray)

    Integer for axis: nil, otherwise a reduced CA_INT64 CArray.



32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/carray/methods/nunique.rb', line 32

def nunique (axis: nil, keep_axis: false)
  # Per-fiber single-pass seen-set hash (C __nunique__), one lane per dtype
  # family (numeric widen / NaN collapse, object rb_hash + rb_eql, fixlen
  # byte-hash + memcmp). Masked cells are skipped; the accumulator is a no-op
  # (the distinct count is the interned-key count).
  if axis.nil?
    # Whole-array distinct count: flatten to 1-D and reduce its only axis,
    # then read the single reduced cell as an Integer.
    flatten.send(:__nunique__, 0, false)[0]
  else
    __nunique__(normalize_axis(axis, "nunique"), keep_axis)
  end
end

#pack_bitsCArray

Packs a 1-D boolean / 0-1 uint8 CArray of length n into a uint8 CArray of ceil(n / 8) bytes, LSB-first within each byte. Exact inverse of the .bitarray view's unpack direction: for any packed uint8 array p, p.bitarray.reshape(-1)[0...p.elements * 8].pack_bits round-trips to p. Tail bits of the last byte (when n is not a multiple of 8) are zero-filled. The byte order matches the packed-bit convention used by Apache Arrow validity bitmaps and PEP 3118 ? / _Bool at the bit level.

Returns:

  • (CArray)

    uint8 CArray of shape [ceil(n / 8)].

Raises:

  • (ArgumentError)

    when the receiver is not 1-D or its data_type is not one of CA_BOOLEAN / CA_UINT8 / CA_INT8.

Raises:

  • (ArgumentError)


64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/carray/methods/bit_string.rb', line 64

def pack_bits
  unless data_type == CA_BOOLEAN || data_type == CA_UINT8 || data_type == CA_INT8
    raise ArgumentError,
      "pack_bits: expected CA_BOOLEAN / CA_UINT8 / CA_INT8 (got #{data_type_name})"
  end
  raise ArgumentError, "pack_bits: 1-D CArray expected (got rank #{rank})" unless rank == 1
  n = elements
  n_bytes = (n + 7) / 8
  packed = CArray.uint8(n_bytes) { 0 }
  return packed if n == 0
  packed.bitarray.reshape(-1)[0..n-1] = self
  packed
end

#paste(offset, src) ⇒ self

Sets self at offset by copying src. Out-of-bounds cells (src extending past self's edge or before its origin) are silently dropped via CAWindow's interior-only PUT scatter.

Parameters:

  • offset (Array<Integer>)

    starting indices, length equal to self.ndim.

  • src (CArray)

    source array.

Returns:

  • (self)

Raises:

  • (ArgumentError)

    when offset.length != self.ndim.

Raises:

  • (ArgumentError)


187
188
189
190
191
192
# File 'lib/carray/basics.rb', line 187

def paste (offset, src)
  raise ArgumentError, "offset length must equal ndim" if offset.length != ndim
  ranges = offset.each_with_index.map { |o, i| o...(o + src.shape[i]) }
  self.window(*ranges)[] = src
  self
end

#put_nd(indices, values) ⇒ self

Sets self at the N-D coordinates given by indices to values. Inverse of #gather_nd.

self.shape    = (D0, ..., D_{K-1}, *rest)
indices.shape = (*outer, K)
values          broadcast to (*outer, *rest)

Duplicate coordinates in indices use last-write-wins semantics, matching put_along_axis. Accumulate semantics (+=) are not provided here; route to the scatter_*! family instead (e.g. self.flatten.scatter_add!(flat_addr, vals)).

Parameters:

  • indices (CArray, Array<CArray, Integer>)

    stacked integer CArray shaped (*outer, K), or an Array of K per-axis coordinate arrays (same forms as #gather_nd).

  • values (CArray, Numeric)

    values broadcastable to (*outer, *rest).

Returns:

  • (self)

Raises:

  • (ArgumentError)

    on the same conditions as #gather_nd.

  • (IndexError)

    when a coordinate is out of range on any axis.



92
93
94
95
96
# File 'lib/carray/methods/gather_nd.rb', line 92

def put_nd (indices, values)
  flat_addr, _outer, _rest = gather_nd_flat_addr(indices, "put_nd")
  self.flatten[flat_addr] = values
  self
end

#realCArray

Returns the real part of self as a zero-copy view. For a complex array the view is a mutable CAField into the real-part slot; for a real numeric array it is a CARefer over self. Writing to the view updates self in place.

Returns:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/carray/complex.rb', line 61

def real
  if not @__real__
    if complex?
      @__real__ = case data_type
                  when CA_CMPLX64
                    field(0, CA_FLOAT32)
                  when CA_CMPLX128
                    field(0, CA_FLOAT64)
                  end
    else
      @__real__ = self[]
    end
  end
  @__real__
end

#real=(val) ⇒ Object

Sets the real-part slot to val via #real.

Parameters:

  • val (CArray, Numeric)

    value to broadcast.

Returns:



81
82
83
# File 'lib/carray/complex.rb', line 81

def real= (val)
  real[] = val
end

#real?Boolean?

Returns whether every element of self is real (imaginary part is zero for complex arrays; always true for real numeric arrays; nil for non-numeric arrays).

Returns:



125
126
127
128
129
130
131
132
133
# File 'lib/carray/complex.rb', line 125

def real?
  if complex?
    imag.eq(0).all
  elsif numeric?
    true
  else
    nil
  end
end

#replace_where(cond, b) ⇒ CArray

Returns a copy of self with cells where cond is true replaced by b. Functional sibling of the destructive indexer a[cond] = b; mask handling matches the indexer. Preserves self's data_type (unlike #then_else, which promotes via CArray.result_type).

Parameters:

  • cond (CArray)

    boolean selector; same shape as self or broadcastable.

  • b (CArray, Numeric, Object)

    replacement value(s).

Returns:

Raises:

  • (ArgumentError)

    when cond is not a boolean CArray.



74
75
76
77
78
79
80
81
82
# File 'lib/carray/conditional.rb', line 74

def replace_where (cond, b)
  unless cond.is_a?(CArray) && cond.boolean?
    raise ArgumentError,
          "replace_where: cond must be a boolean CArray (data_type == CA_BOOLEAN)"
  end
  result = self.copy
  result[cond] = b.is_a?(CArray) ? b[cond] : b
  result
end

#resize(*newdim, fill_value: 0) ⇒ CArray

Returns self resized to newdim. The original data is placed at offset 0 (positive size) or right-aligned (negative size); the new area outside the original region is filled with fill_value, or -- for fixlen storage -- left as zero bytes. Pass UNDEF to mask the new area.

Each entry of newdim is: nil to keep the current shape[i], a positive Integer for a new size with the original left-aligned at offset 0, or a negative Integer d for a new size |d| with the original right-aligned.

Works on Face arrays (CARecord / CATime / ...) and plain fixlen: the resize is done on the storage layout (preserving bytes) and re-wrapped as the same Face.

Parameters:

  • newdim (Array<Integer, nil>)

    new shape spec, one entry per axis.

  • fill_value (Object) (defaults to: 0)

    value for the extended area.

Returns:



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/carray/methods/resize.rb', line 23

def resize (*newdim, fill_value: 0)
  raise "ndim mismatch" if newdim.size != ndim
  offset = Array.new(ndim, 0)
  newdim = newdim.each_with_index.map do |d, i|
    case d
    when nil
      shape[i]
    when Integer
      size = d.abs
      offset[i] = size - shape[i] if d < 0
      size
    else
      raise "invalid dimension size"
    end
  end
  face_parent = self.face? ? self : nil
  src = self
  src = src.parent while src.face?
  dt    = src.data_type
  bytes = (dt == :fixlen) ? src.bytes : nil
  out = CArray.new(dt, newdim, bytes: bytes)
  # Fill the new area: numeric storage takes fill_value as-is; fixlen
  # storage cannot hold a numeric 0, so leave zero bytes and honor only
  # UNDEF (mask) or an explicit String fill.
  if dt != :fixlen || fill_value.equal?(UNDEF) || fill_value.is_a?(String)
    out[] = fill_value
  end
  out.mask.paste(offset, src.false) if out.has_mask?
  out.paste(offset, src)
  out = out.face_lift(face_parent) if face_parent
  out
end

#save_arrow_tensor(filename) ⇒ self

Writes self to filename as an Arrow tensor IPC message.

Experimental, and the name is provisional. Rejects a masked or non-numeric array; see ArrowTensor for the type policy.

Parameters:

  • filename (String)

    path to write.

Returns:

  • (self)

See Also:



397
398
399
400
# File 'lib/carray/arrow_tensor.rb', line 397

def save_arrow_tensor (filename)
  File.open(filename, "wb") { |io| ArrowTensor.write(self, io) }
  self
end

#scale(xa, xb) ⇒ CArray

Returns a fresh CArray shaped like self holding elements evenly spaced values from xa to xb inclusive.

Parameters:

  • xa (Numeric)

    first value.

  • xb (Numeric)

    last value.

Returns:



385
386
387
# File 'lib/carray/basics.rb', line 385

def scale (xa, xb)
  template.scale!(xa, xb)
end

#scale!(xa, xb) ⇒ self

Sets self to elements evenly spaced float64 values from xa to xb inclusive.

Parameters:

  • xa (Numeric)

    first value.

  • xb (Numeric)

    last value.

Returns:

  • (self)


373
374
375
376
377
# File 'lib/carray/basics.rb', line 373

def scale! (xa, xb)
  xa = xa.to_f
  xb = xb.to_f
  seq!(xa, (xb-xa)/(elements-1))
end

#set_attr(key, value) ⇒ Object

Sets attribute key to value on self (not on any parent). Values are validated as JSON-compatible plus non-finite Floats (String / Numeric incl. Infinity / NaN / true / false / nil / Symbol / Array / Hash); Symbol values are coerced to String on store.

Parameters:

  • key (Symbol, String)

    attribute key.

  • value (Object)

    JSON-compatible value.

Returns:

  • (Object)

    the coerced stored value.

Raises:

  • (TypeError)

    when key or value is not accepted.



59
60
61
62
# File 'lib/carray/attribute.rb', line 59

def set_attr (key, value)
  attr_validate_value(value)
  (@attr ||= {})[attr_normalize_key(key)] = attr_coerce_value(value)
end

#snap(step, offset: 0.0, direction: :round) ⇒ CArray

Returns each element snapped to a point on the uniform grid ..., -step + offset, offset, step + offset, 2*step + offset, .... The rounding rule follows direction: (default :round, matching CArray#round; :floor snaps toward -inf, :ceil toward +inf).

The output has the same dtype as self (integer input is coerced to float internally when step / offset are floats, following normal arithmetic promotion). NaN / Inf are preserved as a mask on the output; the rounding kernels map NaN to 0.0, which would silently land NaN cells on the grid, so NaN is detected explicitly on the scaled chain.

Use snap_to(list) when the target grid is non-uniform; use bin(vmin, vmax, step) when the desired output is a bin index.

Examples:

temp.snap(0.5)                        # nearest 0.5 K grid point
temp.snap(0.5, offset: 0.25)          # to 0.25, 0.75, 1.25, ... (bin centers)
temp.snap(0.5, direction: :floor)     # bin lower edge
temp.snap(0.5, direction: :ceil)      # bin upper edge

Parameters:

  • step (Numeric)

    positive grid spacing.

  • offset (Numeric) (defaults to: 0.0)

    phase / origin of the grid; the grid always passes through offset.

  • direction (:round, :floor, :ceil) (defaults to: :round)

    rounding rule; :round picks the nearest grid point (ties half-away-from-zero), :floor picks the grid point at or below the value, :ceil picks the grid point at or above.

Returns:

  • (CArray)

    snapped values, same shape as self.

Raises:

  • (ArgumentError)

    when step <= 0 or direction is not one of the accepted symbols.

Raises:

  • (ArgumentError)


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/carray/methods/snap.rb', line 35

def snap(step, offset: 0.0, direction: :round)
  raise ArgumentError, "snap: step must be > 0" unless step > 0
  unless [:round, :floor, :ceil].include?(direction)
    raise ArgumentError,
          "snap: direction must be :round / :floor / :ceil " \
          "(got #{direction.inspect})"
  end

  scaled = (self - offset) / step

  # Detect NaN / Inf before rounding (which maps NaN -> 0.0 silently).
  invalid_mask = scaled.float? ? scaled.is_invalid : nil

  out = scaled.send(direction) * step + offset

  if invalid_mask && invalid_mask.count(true) > 0
    out.mask = out.has_mask? ? (out.mask | invalid_mask) : invalid_mask
  end

  out
end

#snap_to(list, lfill: :clamp, ufill: :clamp, direction: :round) ⇒ CArray

Returns each element snapped to a value in list (non-uniform grid). The rounding rule follows direction: (default :round for nearest neighbour; :floor picks the list value at or below the sample, :ceil at or above), and delegates to locate_nearest_addr(direction:).

list must be a 1-D ascending numeric CArray (or convertible via CArray.wrap_readonly). Out-of-range and NaN handling follows the same pattern as bin_to, but with an additional :clamp sentinel that snaps out-of-range cells to the nearest list end.

  • :clamp (default) — below-range cells become list[0], above-range cells become list[-1].
  • nil — the side is masked in the output.
  • any other value — the side is filled with that value.

NaN / masked input cells are always masked in the output, regardless of lfill / ufill.

Use snap(step, offset:) when the target grid is uniform; use bin_to(edges) when the output is a bin index against half-open intervals rather than a nearest-value snap.

Examples:

temp.snap_to([270.0, 280.0, 290.0, 300.0])                   # clamp OOB
temp.snap_to(grid, lfill: nil, ufill: nil)                   # mask OOB
rain.snap_to([0.0, 1.0, 5.0, 20.0], direction: :floor)       # list value at or below

Parameters:

  • list (CArray, Array<Numeric>)

    1-D ascending grid, at least one value.

  • lfill (:clamp, nil, Numeric) (defaults to: :clamp)

    handling for below-list[0] cells.

  • ufill (:clamp, nil, Numeric) (defaults to: :clamp)

    handling for above-list[-1] cells.

  • direction (:round, :floor, :ceil) (defaults to: :round)

    rounding rule; forwarded to locate_nearest_addr.

Returns:

  • (CArray)

    snapped values with list's data_type, same shape as self.

Raises:

  • (ArgumentError)

    when list is not 1-D or is empty, or direction is not one of the accepted symbols.

Raises:

  • (ArgumentError)


97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/carray/methods/snap.rb', line 97

def snap_to(list, lfill: :clamp, ufill: :clamp, direction: :round)
  ref = list.is_a?(CArray) ? list : CArray.wrap_readonly(list, self.data_type)
  raise ArgumentError, "snap_to: list must be 1-D" unless ref.ndim == 1
  n = ref.elements
  raise ArgumentError, "snap_to: list must have at least one value" if n < 1

  if n == 1
    # Degenerate: every finite cell snaps to the only value.
    out = CArray.new(ref.data_type, shape).fill(ref[0])
    out.mask = self.mask.to_ca if self.has_mask?
    if self.float?
      inv = self.is_invalid
      if inv.count(true) > 0
        out.mask = out.has_mask? ? (out.mask | inv) : inv
      end
    end
    return out
  end

  # `locate_nearest_addr` (via `linear_section`) accepts a 1-D `val` only;
  # flatten multi-D input and reshape the result back to preserve the
  # element-wise semantic on any shape.
  if ndim > 1
    return reshape(-1).snap_to(ref, lfill: lfill, ufill: ufill,
                               direction: direction).reshape(*shape)
  end

  # locate_nearest_addr returns int64 indices; OOB (below / above / NaN)
  # cells come back masked. We split OOB into below / above with
  # explicit comparisons so the two sides can be filled independently.
  idx = self.locate_nearest_addr(ref, direction: direction)
  out = ref.project(idx)

  below = self.lt(ref[0])
  above = self.gt(ref[-1])

  case lfill
  when :clamp then out[below] = ref[0]
  when nil    then # leave masked (locate_nearest_addr already masked OOB)
  else             out[below] = lfill
  end

  case ufill
  when :clamp then out[above] = ref[-1]
  when nil    then # leave masked
  else             out[above] = ufill
  end

  # Propagate input mask (locate_nearest_addr / project do not forward
  # `self`'s mask on their own; a masked input cell must produce a
  # masked output cell regardless of the fill options above).
  if self.has_mask?
    m = self.mask.to_ca
    out.mask = out.has_mask? ? (out.mask | m) : m
  end

  out
end

#source_codeString

Returns a Ruby source-like string that would reconstruct self, combining the type/shape descriptor with a pretty printed value block. Useful for embedding fixtures in scripts.

Returns:

  • (String)


250
251
252
253
254
255
256
257
258
259
260
# File 'lib/carray/inspect.rb', line 250

def source_code
  text = [
    desc,
    " { ",
    self.to_a.pretty_inspect.split("\n").map{|s|
      " " * (desc.length+3) + s
    }.join("\n").lstrip,
    " }"
  ].join
  return text
end

#span(range) ⇒ CArray

Returns a fresh CArray shaped like self filled with the linear sequence produced by #span!. Float arrays only.

Parameters:

  • range (Range<Numeric>)

    value range to span.

Returns:

Raises:

  • (ArgumentError)

    when self is not a float array.



363
364
365
# File 'lib/carray/basics.rb', line 363

def span (range)
  return template.span!(range)
end

#span!(range) ⇒ self

Sets self to a linear sequence over range, with the step chosen so that range.end (or range.end when the range is exclusive-end, treated as the limit not reached) determines the endpoint. Concretely:

  • inclusive range a..b: self[0] == a, self[-1] == b, intermediate values are evenly spaced.
  • exclusive range a...b: self[0] == a, `self[-1] == a + (N-1)
    • (b-a)/N(endpointb` is not reached).

Only float arrays are supported. Integer arrays raise — "N evenly-spaced integers" is not a well-defined concept; the error message shows the two idioms that cover the two distinct integer use cases:

  • (A) N points with both endpoints hitting a and b exactly (linspace-like): use the manual integer form CArray.int32(N).seq * (b - a) / (N - 1) + a, or sample as float then cast: CArray.float64(N).span(a.to_f..b.to_f).int32.
  • (B) N labels distributed uniformly over the value range so each of the (b - a + 1) values appears approximately the same number of times (bucket distribution): use CArray.int32(N).seq * (b - a + 1) / N + a.

Parameters:

  • range (Range<Numeric>)

    value range to span.

Returns:

  • (self)

Raises:

  • (ArgumentError)

    when self is not a float array.



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/carray/basics.rb', line 339

def span! (range)
  unless float?
    raise ArgumentError,
          "span!: integer arrays are ambiguous — 'N evenly-spaced " \
          "integers' has two distinct meanings. Pick the one you want:\n" \
          "  (A) N points hitting both endpoints exactly (linspace-like):\n" \
          "      CArray.int32(N).seq * (b - a) / (N - 1) + a\n" \
          "      or  CArray.float64(N).span(a.to_f..b.to_f).int32\n" \
          "  (B) N labels distributed uniformly over range values:\n" \
          "      CArray.int32(N).seq * (b - a + 1) / N + a"
  end
  first = range.begin.to_r
  last  = range.end.to_r
  step = range.exclude_end? ? (last-first)/elements : (last-first)/(elements-1)
  seq!(first, step)
  return self
end

#split(axis:) ⇒ Array<CArray>

Split self along a single axis into an Array of (ndim-1)-D slices, each a writable CABlock view. The exact inverse of CArray.stack -- split's slices are all the same shape, so they round-trip back through stack:

CArray.stack(a.split(axis: k), axis: k) == a

a = CA_INT([[1,2,3], [4,5,6]]) a.split(axis: 0) #=> <[1,2,3]>, <[4,5,6]> a.split(axis: 1) #=> <[1,4]>, <[2,5]>, <[3,6]>

3.0 breaking:

  • returns a Ruby Array of views (was an object CArray), so it round-trips with CArray.stack (which takes an Array)
  • +axis:+ takes a single Integer (the multi-axis Array form, which returned an N-D object grid, is no longer accepted)
  • pieces are CABlock views, NOT copies; writing through a piece mutates +self+. Chain +.copy+ / +.to_ca+ for independent entities.

Returns an Array of (ndim-1)-D writable CABlock views obtained by splitting self along axis. Inverse of CArray.stack. Slices share storage with self; chain .copy for independent entities.

Parameters:

  • axis (Integer)

    axis to split along.

Returns:

Raises:

  • (ArgumentError)

    when axis is not a single Integer.



251
252
253
254
255
256
257
258
259
260
261
# File 'lib/carray/stack.rb', line 251

def split (axis:)
  if axis.is_a?(Array)
    raise ArgumentError, "split: axis must be a single Integer"
  end
  k = normalize_axis(axis, "split")
  (0...shape[k]).map do |i|
    idx = [nil] * ndim
    idx[k] = i
    self[*idx]
  end
end

#stStruct

Returns a Ruby Struct view exposing every CAStruct member of self as a Struct attribute holding the corresponding member column. Cached per receiver.

Returns:

  • (Struct)

Raises:



166
167
168
169
170
171
172
173
174
175
176
# File 'lib/carray/struct.rb', line 166

def st
  unless has_data_class?
    raise CAStruct::Error, "carray does not have a data_class"
  end
  unless @struct
    struct_class = Struct.new(nil, *data_class::MEMBERS)
    members = data_class::MEMBERS.map{|name| self[name]}
    @struct = struct_class.new(*members)
  end
  return @struct
end

#stack(*others, axis: 0, data_type: nil) ⇒ CArray

Instance-side stack: build a new K-stack from [self] + others along the new K axis at position axis:. Always treats self as a parent (= even when self is a CAStack, the resulting stack has self as one of its parents, NOT flat-appended into self's parents).

For flat-appending into an existing CAStack (= same k_axis, parents extended), use CAStack#append.

3.0: high-level Face-aware surface, mirrors CArray.stack(list, axis:).

Returns a K-stack view built from [self] + others along a new K axis at position axis. Always treats self as one parent, even when self is a CAStack; use CAStack#append to flat-append into an existing CAStack.

Parameters:

  • others (Array<CArray>)

    additional parents.

  • axis (Integer) (defaults to: 0)

    position of the new K axis.

  • data_type (Symbol, Integer, nil) (defaults to: nil)

    result data_type.

Returns:

Raises:

  • (ArgumentError)

    when no others are given.

Raises:

  • (ArgumentError)


221
222
223
224
# File 'lib/carray/stack.rb', line 221

def stack (*others, axis: 0, data_type: nil)
  raise ArgumentError, "stack: at least one other parent required" if others.empty?
  CArray.stack([self] + others, axis: axis, data_type: data_type)
end

#strip_mask(fill = MASK_FILL_UNSET, method: nil, axis: nil) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/carray/mask_gap_fill.rb', line 68

def strip_mask (fill = MASK_FILL_UNSET, method: nil, axis: nil)
  if method
    unless fill.equal?(MASK_FILL_UNSET)
      raise ArgumentError,
            "strip_mask: pass either a constant fill value or method:, not both"
    end
    return __gap_fill__(method, axis)
  end
  if fill.equal?(MASK_FILL_UNSET)
    raise ArgumentError, "strip_mask: a fill value is required (or method:)"
  end
  __strip_mask_const__(fill)
end

#then_else(x, y) ⇒ CArray

Returns a ternary selection on self (a boolean CArray), reading as "if self then x else y". The boolean 2-way case of #choose that additionally propagates self's mask (UNDEF in self produces UNDEF in the result).

Parameters:

Returns:

  • (CArray)

    new array with the same shape as self.

Raises:

  • (ArgumentError)

    when self is not a boolean CArray.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/carray/conditional.rb', line 32

def then_else (x, y)
  # Guard: self must be boolean.  Integer / float receiver would be
  # silently reinterpreted by the indexer setter (`result[self] = ...`)
  # as an index array, producing surprising scatter rather than the
  # intended ternary select.  Fail fast.
  unless self.boolean?
    raise ArgumentError,
          "then_else: receiver must be a boolean CArray (data_type == CA_BOOLEAN), got #{self.data_type}"
  end
  # Promote data_type from both branches via CArray.result_type
  # (a CScalar contributes its own data_type, so CA_INT32(0) keeps int32
  # where a bare Ruby Integer would widen to int64).
  dt = CArray.result_type(x, y)
  # A CScalar (scalar? CArray) is treated as a scalar value, not as a
  # self-shaped operand: full CArray -> gather/copy, scalar -> broadcast.
  y_full = y.is_a?(CArray) && !y.scalar?
  result =
    if y_full
      y.data_type == dt ? y.copy : y.to_type(dt)
    else
      CArray.new(dt, self.shape).fill(y.is_a?(CArray) ? y[0] : y)
    end
  x_full = x.is_a?(CArray) && !x.scalar?
  result[self] = x_full ? x[self] : x
  # Propagate cond's mask: UNDEF in self -> UNDEF in result.
  if self.has_mask?
    result[self.is_masked] = UNDEF
  end
  result
end

#time(unit: :ns, origin: nil) ⇒ CATime

Note:

An int64 receiver is wrapped zero-copy. A narrower integer type is widened to int64 first (a copy); a Float / non-integer type raises (cast it explicitly if the truncation is intended).

Returns self as a CATime on the unit grid. With origin nil, self's int64 values are taken as tick indices already anchored to the Unix epoch (zero-copy Face wrap). With origin given, self's indices are relative to origin and are rebased to the epoch (a new int64 array is built; the origin is not stored).

Parameters:

  • unit (Resolution, Symbol, String) (defaults to: :ns)

    grid resolution.

  • origin (Time, String, Integer, DateTime, nil) (defaults to: nil)

    base instant that self's indices are counted from (default: the Unix epoch).

Returns:



1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
# File 'lib/carray/time.rb', line 1689

def time(unit: :ns, origin: nil)
  res = CATime::Resolution.parse(unit)
  src = _time_int64_storage
  if origin.nil?
    CATime.wrap(src, unit: res)
  else
    o = CArray._epoch_tick_index(origin, res)
    (src + o).time(unit: res)
  end
end

#timedelta(unit: :ns) ⇒ CATimedelta

Note:

An int64 receiver is wrapped zero-copy; a narrower integer type is widened to int64 first (a copy); a Float / non-integer raises.

Returns self re-wrapped as a zero-copy CATimedelta view with the given unit.

Parameters:

  • unit (Resolution, Symbol, String) (defaults to: :ns)

    duration resolution.

Returns:



1707
1708
1709
# File 'lib/carray/time.rb', line 1707

def timedelta(unit: :ns)
  CATimedelta.wrap(_time_int64_storage, unit: unit)
end

#to_bit_string(nb) ⇒ String

Returns a packed-bit byte string built from self, using nb bits per element.

Parameters:

  • nb (Integer)

    bits per element.

Returns:

  • (String)

    byte string of length ceil(nb * elements / 8).



11
12
13
14
15
16
# File 'lib/carray/methods/bit_string.rb', line 11

def to_bit_string (nb)
  hex = CArray.uint8(((nb*elements)/8.0).ceil)
  hex.bits[nil].paste([0], self.bits[false,[(nb-1)..0]].flatten)
  hex.bits[] = hex.bits[nil,[-1..0]]
  return hex.to_s
end

#trueCArray

Returns a boolean CArray of the same shape as self filled with true.

Returns:



168
169
170
# File 'lib/carray/basics.rb', line 168

def true ()
  return template(:boolean) { 1 }
end

#union(other, sort: false) ⇒ CArray

Returns a 1-D CArray of the distinct values appearing in either self or other, in self-then-other first-appearance order. See #intersection for the shared semantics and options.

Parameters:

  • other (CArray, Array, Range)

    promoted with self.

  • sort (Boolean) (defaults to: false)

    when true, return the values sorted ascending (a merged, ordered set — e.g. a common time axis).

Returns:

  • (CArray)

    1-D CArray of the combined distinct values.



90
91
92
93
94
# File 'lib/carray/methods/is_in.rb', line 90

def union (other, sort: false)
  a, b = promote_value_set(other)
  r = a.__send__(:__union__, b)
  sort ? r.sort : r
end

#unique(sort: false) ⇒ CArray

Returns a 1-D CArray of the distinct values of self, in first-appearance (row-major flatten) order. This is the compressing counterpart of #mask_duplicates (which marks without compressing): because different fibers may hold different numbers of distinct values, compression is only well-defined over the whole array, so unique is always flat.

Named unique (not uniq) because distinctness is value-based like NumPy / pandas unique: unlike Ruby Array#uniq it collapses all NaN to a single value (see below), so the name avoids promising Array#uniq semantics.

Masked cells do not participate and never appear in the result; an all-masked array yields an empty CArray.

Distinctness follows == for numeric dtypes, with two float special cases so the result matches value-based expectations: all NaN collapse to a single distinct value (rather than one per cell) and -0.0 / +0.0 are the same value. The value kept for each key is the first one seen, so a leading -0.0 keeps its sign. For CA_OBJECT / CA_FIXLEN distinctness follows Ruby eql? / hash; note that Ruby does not collapse distinct NaN objects, so a CA_OBJECT array of Float NaN is not collapsed (unlike the numeric path).

A time array (CATime / CATimedelta) answers with its own type on its own unit: the distinct values are values, so the array comes back as itself rather than as raw storage ticks.

Parameters:

  • sort (Boolean) (defaults to: false)

    when true, return the distinct values sorted ascending instead of in first-appearance order.

Returns:

  • (CArray)

    1-D CArray of the distinct values, same dtype as self.



37
38
39
40
41
42
43
44
45
# File 'lib/carray/methods/unique.rb', line 37

def unique (sort: false)
  # Single-pass seen-set hash (C __unique_flat__), one lane per dtype family:
  # integer widens to a 64-bit key; float uses the bitwise key with all-NaN
  # collapsed and -0.0 / +0.0 normalized; object keys on rb_hash + rb_eql and
  # fixlen on a byte-hash + memcmp, both reproducing Ruby Hash distinctness.
  # Masked cells are skipped in the kernel.
  levels = __unique_flat__
  sort ? levels.sort : levels
end

#unmask(fill = MASK_FILL_UNSET, method: nil, axis: nil) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/carray/mask_gap_fill.rb', line 43

def unmask (fill = MASK_FILL_UNSET, method: nil, axis: nil)
  if method
    unless fill.equal?(MASK_FILL_UNSET)
      raise ArgumentError,
            "unmask: pass either a constant fill value or method:, not both"
    end
    held = __gap_fill__(method, axis)
    # Copy the filled values in place.  A Face writes through its storage: a
    # bulk store into its surface would try to cast the storage values to the
    # surface type (int64 ticks to fixlen, for a time array).
    if face?
      parent.value[] = held.parent.value
    else
      value[] = held.value
    end
    if held.has_mask?
      self.mask = held.mask              # residual leading/trailing mask
    else
      __unmask_const__                   # fully filled: drop the mask
    end
    return self
  end
  fill.equal?(MASK_FILL_UNSET) ? __unmask_const__ : __unmask_const__(fill)
end

#validity_bitsCArray?

Returns a packed uint8 CArray where bit i is 1 iff cell i of the receiver is not masked (LSB-first, length ceil(elements / 8)). Returns nil when the receiver has no mask; consumers such as Arrow treat a missing bitmap as "all valid", so nil is the correct omission-signalling value. Equivalent to is_not_masked.pack_bits when a mask is present.

Returns:

  • (CArray, nil)

    uint8 CArray of shape [ceil(elements / 8)], or nil when no mask is set.



87
88
89
90
# File 'lib/carray/methods/bit_string.rb', line 87

def validity_bits
  return nil unless has_mask?
  is_not_masked.reshape(-1).pack_bits
end

#value_counts(sort: false) ⇒ Array(CArray, CArray)

Returns [values, counts], the distinct values of self paired with the number of times each occurs. values is a 1-D CArray of self's dtype; counts is a 1-D CA_INT64 where counts[i] is the number of occurrences of values[i]. This is the frequency- table member of the value-hash discovery family (#unique, #mask_duplicates, #nunique); like #unique it always flattens, because per-fiber distinct counts would be ragged.

By default the pairs are in first-appearance (row-major flatten) order, matching #unique. sort: reorders both arrays together:

  • false (default) — first-appearance order.
  • :count — descending frequency; ties keep first-appearance order (deterministic).
  • :value — ascending value (float NaN sorts last, as in unique(sort: true)).

Masked cells do not participate and never appear; an all-masked array yields two empty CArrays.

Numeric distinctness follows == with two float special cases so the result matches value-based expectations: all NaN collapse to a single value (their counts add up) and -0.0 / +0.0 are the same value (the first-seen value is kept, so a leading -0.0 keeps its sign). For CA_OBJECT / CA_FIXLEN distinctness follows Ruby eql? / hash; Ruby does not collapse distinct NaN objects, so a CA_OBJECT array of Float NaN is not collapsed (unlike numeric).

The values keep self's type (a CATime comes back as a CATime on its own unit); the counts are always a plain :int64 CArray.

Parameters:

  • sort (false, :count, :value) (defaults to: false)

    pair ordering.

Returns:



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/carray/methods/value_counts.rb', line 37

def value_counts (sort: false)
  unless [false, :count, :value].include?(sort)
    raise ArgumentError, "value_counts: sort must be false, :count, or :value"
  end
  # Single-pass frequency-table hash (C __value_counts_flat__), one lane per
  # dtype family: integer widens to a 64-bit key; float uses the bitwise key
  # with all NaN collapsed and -0.0 / +0.0 normalized; object keys on rb_hash +
  # rb_eql and fixlen on a byte-hash + memcmp, both reproducing Ruby Hash
  # distinctness. Masked cells are skipped in the kernel.
  values, counts = __value_counts_flat__
  case sort
  when :count
    # Descending count, ties broken by first-appearance index (stable).
    c = counts.to_a
    order = (0...c.size).sort_by { |i| [-c[i], i] }
    [ values[CArray.int64(order.size) { |i| order[i] }],
      counts[CArray.int64(order.size) { |i| order[i] }] ]
  when :value
    # Ascending value; NaN (numeric) or non-comparable last. Build the
    # permutation with an explicit NaN-last key so float NaN doesn't blow up
    # the Ruby sort, then gather both arrays through it.
    v = values.to_a
    order = (0...v.size).sort_by do |i|
      x = v[i]
      nan = x.is_a?(Float) && x.nan?
      [nan ? 1 : 0, nan ? 0 : x, i]
    end
    [ values[CArray.int64(order.size) { |i| order[i] }],
      counts[CArray.int64(order.size) { |i| order[i] }] ]
  else
    [values, counts]
  end
end

#windows(*ranges, bounds: :skip, fill_value: nil) ⇒ CAWindowIterator

Returns a CAWindowIterator rolling a per-axis offset window over self. Each ranges[i] is a lo..hi offset span (a.windows(-1..1) is a centred width-3 window); bounds: selects the margin policy. With no ranges (a.windows(a.window(...)) passing a CAWindow view) the geometry is read from the view for backward compatibility.

Parameters:

  • ranges (Array<Range>)

    per-axis offset ranges.

  • bounds (Symbol) (defaults to: :skip)

    :skip / :nearest / :truncate.

  • fill_value (Object, nil) (defaults to: nil)

    constant margin value.

Returns:



649
650
651
652
653
654
# File 'lib/carray/window_iterator.rb', line 649

def windows (*ranges, bounds: :skip, fill_value: nil)
  if ranges.size == 1 && ranges[0].is_a?(CArray) && ranges[0].obj_type == CA_OBJ_WINDOW
    return CAWindowIterator.new(ranges[0])
  end
  CAWindowIterator.new(self, *ranges, bounds: bounds, fill_value: fill_value)
end