Class: CABlockIterator

Inherits:
CAIterator show all
Defined in:
lib/carray/block_iterator.rb

Overview

Non-overlapping tile reduction dispatcher — the Block member of the iterator family (sibling of CASlabIterator / CAWindowIterator / CACategoricalIterator). Where a window iterator folds an overlapping window per anchor, a block iterator folds each non-overlapping tile of a fixed per-axis size, so the result is a tile grid: pooling, downsampling, block statistics.

Obtained from CArray#blocks, not constructed directly.

Examples:

bi = a.blocks(3, 3)   # 3x3 non-overlapping tiles
bi.mean               # per-tile mean (average pooling)
bi.max                # per-tile max (max pooling)

Instance Attribute Summary collapse

Attributes inherited from CAIterator

#ndim, #shape

Instance Method Summary collapse

Constructor Details

#initialize(source, *blocks) ⇒ CABlockIterator

Returns a new instance of CABlockIterator.

Builds a block iterator tiling source with a per-axis tile size. Each blocks[i] is either an Integer tile size (offset 0) or a lo..hi range whose length is the tile size and whose start is a leading offset (backward compatible with the 2.0 a.blocks(2..4) form). A single Array argument is taken as the per-axis size list.

Parameters:

  • source (CArray)

    the array to tile.

  • blocks (Array<Integer, Range>)

    per-axis tile sizes (or ranges).



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
# File 'lib/carray/block_iterator.rb', line 69

def initialize (source, *blocks)
  blocks = blocks[0] if blocks.size == 1 && blocks[0].is_a?(Array)

  unless blocks.size == source.ndim
    raise ArgumentError,
          "blocks: expected #{source.ndim} tile sizes (one per axis), " \
          "got #{blocks.size}"
  end

  @sndim   = source.ndim
  offsets  = Array.new(@sndim, 0)
  @sizes   = Array.new(@sndim)
  blocks.each_with_index do |b, i|
    if b.is_a?(Range)
      offsets[i] = b.begin
      @sizes[i]  = b.end - b.begin + (b.exclude_end? ? 0 : 1)
    else
      @sizes[i]  = Integer(b)
    end
    if @sizes[i] < 1
      raise ArgumentError, "blocks: tile size on axis #{i} must be >= 1"
    end
  end

  # Absorb a leading offset with a zero-copy pre-slice, so the tile geometry
  # below always starts at index 0.
  @source =
    if offsets.all?(&:zero?)
      source
    else
      source[*offsets.map { |o| o..-1 }]
    end

  n     = @source.shape
  @q    = @sndim.times.map { |i| n[i] / @sizes[i] }   # full tiles per axis
  @r    = @sndim.times.map { |i| n[i] % @sizes[i] }   # remainder per axis

  # Ceil tile grid: a partial edge tile adds one grid cell on that axis.
  @shape  = @sndim.times.map { |i| @q[i] + (@r[i] > 0 ? 1 : 0) }
  @ndim = @shape.size

  # Trailing tile axes of a block_view: [ndim .. 2*ndim-1].
  @tile_axes = (@sndim...(2 * @sndim)).to_a
  self
end

Instance Attribute Details

#sourceCArray (readonly)

Returns the array being tiled (with any leading offset already applied).

Returns:



118
119
120
# File 'lib/carray/block_iterator.rb', line 118

def source
  @source
end

Instance Method Details

#count(v = <none>) ⇒ CArray

Per-tile count. No argument counts present (non-masked) cells (fewer at a partial edge tile); count(UNDEF) counts masked cells; count(v) counts cells equal to v.

Returns:

  • (CArray)

    tile-grid shaped



227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/carray/block_iterator.rb', line 227

def count (*args)
  return count_not_masked if args.empty?
  out = nil
  each_region do |strip_ranges, tiles, out_ranges|
    view = @source[*strip_ranges].block_view(*tiles)
    # block_view is a CAStride, so #count is not shadowed; dispatch
    # CArray#count explicitly anyway, matching the family regularity.
    red = CArray.instance_method(:count).bind_call(view, *args, axis: @tile_axes)
    out ||= CArray.new(red.data_type, @shape)
    out[*out_ranges] = red
  end
  out
end

#count_maskedCArray

Per-tile count of masked cells. Equals elements - count_not_masked; the OOB cells of a partial edge tile count here.

Returns:



253
254
255
# File 'lib/carray/block_iterator.rb', line 253

def count_masked
  elements - count_not_masked
end

#count_not_maskedCArray

Per-tile count of present (non-masked) cells. For a partial edge tile this is the real cell count (the OOB cells are not present).

Returns:



245
246
247
# File 'lib/carray/block_iterator.rb', line 245

def count_not_masked
  fold(:count_not_masked)
end

#each({ |tile| ... }) {|tile| ... } ⇒ Enumerator, self

Yields each tile as a uniform Π b_i-shaped CArray (partial edge tiles have their out-of-bounds cells masked). Without a block, returns an Enumerator. Per-tile materialize, slow; use a named reduction for speed.

Yield Parameters:

Returns:

  • (Enumerator, self)


464
465
466
467
468
469
470
# File 'lib/carray/block_iterator.rb', line 464

def each
  return to_enum(:each) unless block_given?
  tgv  = tile_grid_view
  nils = Array.new(@sndim, nil)
  CArray.each_index(*@shape) { |*g| yield tgv[*g, *nils] }
  self
end

#elementsCArray

Tile cell count (structural, mask-independent): the constant tile size Π b_i, shaped like the tile grid. Every tile — including a partial edge tile — reports the full size; how many of those cells are real is count_not_masked, and elements = count_not_masked + count_masked.

Returns:



263
264
265
266
267
268
# File 'lib/carray/block_iterator.rb', line 263

def elements
  sz  = @sizes.inject(1) { |p, b| p * b }
  out = CArray.int64(*@shape)
  out[] = sz
  out
end

#map({ |tile| ... }) {|tile| ... } ⇒ CArray, Enumerator

Per-tile element-wise transform: the block receives each tile (a uniform Π b_i-shaped CArray, masked at partial edges) and returns a same-shaped tile (or a scalar to broadcast). The transformed tiles are scattered back into a source-shaped CArray (the out-of-bounds cells of a partial edge tile are dropped). Well-defined because tiles do not overlap; the source is not modified (a new array is returned). Without a block, an Enumerator.

Yield Parameters:

Returns:

  • (CArray, Enumerator)

    source-shaped



510
511
512
513
514
515
516
517
518
# File 'lib/carray/block_iterator.rb', line 510

def map
  return to_enum(:map) unless block_given?
  pout = CArray.new(padded_source.data_type, padded_source.shape)
  pgv  = pout.block_view(*@sizes)
  tgv  = tile_grid_view
  nils = Array.new(@sndim, nil)
  CArray.each_index(*@shape) { |*g| pgv[*g, *nils] = yield(tgv[*g, *nils]) }
  pout[*@sndim.times.map { |i| 0...@source.shape[i] }].copy
end

#max_addrCArray

Per-tile flat source address of the maximum. See #min_addr.

Returns:

  • (CArray)

    tile-grid shaped int64



308
# File 'lib/carray/block_iterator.rb', line 308

def max_addr; winner_addr(:max_index); end

#medianObject

Per-tile median. @return [CArray]



411
412
413
# File 'lib/carray/block_iterator.rb', line 411

def median
  order_stat { |v, axis| v.median(axis: axis) }
end

#min_addrCArray

Per-tile flat SOURCE address of the minimum — which cell of the source holds it, so source.reshape(source.elements)[bi.min_addr] are the tile minima. Unlike min_index (the tile-local position) this indexes back into the original array. An all-masked tile is a masked cell.

Returns:

  • (CArray)

    tile-grid shaped int64



303
# File 'lib/carray/block_iterator.rb', line 303

def min_addr; winner_addr(:min_index); end

#minmax(min_count: nil, fill_value: nil) ⇒ Array<CArray>

Per-tile [min, max] (two tile-grid-shaped CArrays, a single fused pass).

Returns:



275
276
277
278
279
280
# File 'lib/carray/block_iterator.rb', line 275

def minmax (min_count: nil, fill_value: nil)
  kw = {}
  kw[:min_count]  = min_count  unless min_count.nil?
  kw[:fill_value] = fill_value unless fill_value.nil?
  assemble { |view, _| view.minmax(axis: @tile_axes, **kw) }
end

#cumsumCArray #cumprodCArray #cummaxCArray #cumminCArray #cumcountCArray

Overloads:

  • #cumsumCArray

    Per-tile inclusive running sum (float64), source-shaped.

    Returns:

  • #cumprodCArray

    Per-tile inclusive running product (float64), source-shaped.

    Returns:

  • #cummaxCArray

    Per-tile inclusive running maximum (value data type), source-shaped.

    Returns:

  • #cumminCArray

    Per-tile inclusive running minimum (value data type), source-shaped.

    Returns:

  • #cumcountCArray

    Per-tile running count of present cells (int64), source-shaped.

    Returns:



210
211
212
213
214
215
216
217
218
# File 'lib/carray/block_iterator.rb', line 210

[:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev, :all, :any,
 :variancep, :stddevp].each do |op|
  define_method(op) do |min_count: nil, fill_value: nil|
    kw = {}
    kw[:min_count]  = min_count  unless min_count.nil?
    kw[:fill_value] = fill_value unless fill_value.nil?
    fold(op, **kw)
  end
end

#percentile(*pers) ⇒ CArray+

Per-tile percentile(s). One argument returns one CArray, several an array of CArrays (as CArray#percentile).

Returns:



419
420
421
# File 'lib/carray/block_iterator.rb', line 419

def percentile (*pers)
  order_stat { |v, axis| v.percentile(*pers, axis: axis) }
end

#quantileArray<CArray>

Per-tile five-number summary [min, Q1, median, Q3, max] (five CArrays).

Returns:



426
427
428
# File 'lib/carray/block_iterator.rb', line 426

def quantile
  order_stat { |v, axis| v.quantile(axis: axis) }
end

#reduce({ |tile| ... }) {|tile| ... } ⇒ CArray #reduce(init) ⇒ CArray

Overloads:

  • #reduce({ |tile| ... }) {|tile| ... } ⇒ CArray

    Custom per-tile reduction: the block receives each tile (a CArray, masked at partial edges) and returns one value per tile. The escape hatch for statistics not in the named surface.

    Yield Parameters:

    Returns:

    • (CArray)

      tile-grid shaped

  • #reduce(init) ⇒ CArray

    Per-tile fiber fold: each tile's cells are folded element by element from init (masked cells are skipped by CArray#each).

    Parameters:

    • init (Object)

      initial accumulator.

    Returns:

Raises:

  • (LocalJumpError)


483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/carray/block_iterator.rb', line 483

def reduce (*args, data_type: nil, &blk)
  raise LocalJumpError, "no block given (yield)" unless blk
  out  = CArray.new(data_type || CA_OBJECT, @shape)
  tgv  = tile_grid_view
  nils = Array.new(@sndim, nil)
  if args.empty?
    CArray.each_index(*@shape) { |*g| out[*g] = blk.call(tgv[*g, *nils]) }
  else
    init = args[0]
    CArray.each_index(*@shape) do |*g|
      acc = init
      tgv[*g, *nils].each { |e| acc = blk.call(acc, e) }
      out[*g] = acc
    end
  end
  out
end

#sort_addrCArray

Per-tile sort by flat SOURCE address, source-shaped: each tile's cells hold that tile's source addresses in ascending-value order (reading the tile row-major gives the sorted addresses), so source.reshape(source.elements)[bi.sort_addr] is the source sorted within each tile. Multi-axis tiles are flattened (as for the order statistics). A partial edge tile sorts only its present cells. Masked values sort to the tail of their tile (as CArray#sort).

Returns:

  • (CArray)

    source-shaped int64



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/carray/block_iterator.rb', line 348

def sort_addr
  saddr = CArray.int64(*@source.shape).seq!
  out = CArray.int64(*@source.shape)
  out[] = UNDEF
  each_region do |strip_ranges, tiles, _out_ranges|
    vview = @source[*strip_ranges].block_view(*tiles)
    sview = saddr[*strip_ranges].block_view(*tiles)
    grid  = (0...@sndim).map { |i| vview.shape[i] }
    cells = tiles.inject(1) { |p, t| p * t }
    order = vview.copy.reshape(*(grid + [cells])).sort_addr(axis: @sndim)
    src_sorted = sview.reshape(*(grid + [cells])).take_along_axis(order, axis: @sndim)
    out[*strip_ranges].block_view(*tiles)[] = src_sorted.reshape(*(grid + tiles))
  end
  out
end

#wmean(weights) ⇒ CArray

Per-tile weighted mean; weights shaped like one full tile.

Returns:



375
376
377
# File 'lib/carray/block_iterator.rb', line 375

def wmean (weights)
  weighted(weights) { |view, w| view.wmean(w, axis: @tile_axes) }
end

#wsum(weights) ⇒ CArray

Per-tile weighted sum; weights is shaped like one full tile (Π b_i). At a partial edge tile the weight kernel is sliced to the present cells.

Returns:



368
369
370
# File 'lib/carray/block_iterator.rb', line 368

def wsum (weights)
  weighted(weights) { |view, w| view.wsum(w, axis: @tile_axes) }
end