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



221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/carray/block_iterator.rb', line 221

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:



247
248
249
# File 'lib/carray/block_iterator.rb', line 247

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:



239
240
241
# File 'lib/carray/block_iterator.rb', line 239

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)


454
455
456
457
458
459
460
# File 'lib/carray/block_iterator.rb', line 454

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:



257
258
259
260
261
262
# File 'lib/carray/block_iterator.rb', line 257

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



500
501
502
503
504
505
506
507
508
# File 'lib/carray/block_iterator.rb', line 500

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



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

def max_addr; winner_addr(:max_index); end

#medianObject

Per-tile median. @return [CArray]



401
402
403
# File 'lib/carray/block_iterator.rb', line 401

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



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

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:



269
270
271
272
273
274
# File 'lib/carray/block_iterator.rb', line 269

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 dtype), source-shaped.

    Returns:

  • #cumminCArray

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

    Returns:

  • #cumcountCArray

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

    Returns:



204
205
206
207
208
209
210
211
212
# File 'lib/carray/block_iterator.rb', line 204

[:sum, :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:



409
410
411
# File 'lib/carray/block_iterator.rb', line 409

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:



416
417
418
# File 'lib/carray/block_iterator.rb', line 416

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)


473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/carray/block_iterator.rb', line 473

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



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

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:



365
366
367
# File 'lib/carray/block_iterator.rb', line 365

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:



358
359
360
# File 'lib/carray/block_iterator.rb', line 358

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