Class: CAWindowIterator

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

Overview

Rolling (sliding-window) reduction dispatcher — the Window member of the iterator family (sibling of CASlabIterator / CABlockIterator / CACategoricalIterator). It folds an overlapping window centred on every anchor cell, so the result is shaped like the source rather than reduced.

Obtained from CArray#windows, not constructed directly.

Examples:

sw = a.windows(-1..1)     # width-3 window per anchor
sw.mean                   # rolling mean, shaped like a
sw.correlate(kernel)      # bounded cross-correlation

Instance Attribute Summary collapse

Attributes inherited from CAIterator

#ndim, #shape

Instance Method Summary collapse

Constructor Details

#initialize(source, ranges, bounds: :skip, fill_value: nil) ⇒ CAWindowIterator

Returns a new instance of CAWindowIterator.

Builds a window iterator over source with a per-axis offset range. Each ranges[i] is a lo..hi giving the window's offset span around an anchor (a.windows(-1..1) is a centred width-3 window; 0..2 is forward-looking). bounds: selects the margin policy (:skip / :nearest / :truncate). fill_value: is a constant margin value (an escape for :constant padding); when given it overrides :skip.

For backward compatibility initialize(window_view) accepts a CAWindow view (the old CAWindowIterator.new(a.window(...)) form): the geometry (offset ranges, bounds, fill value) is read back from the view.

Parameters:

  • source (CArray, CAWindow)

    the array to roll over, or a CAWindow view to read the geometry from.

  • 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, overriding :skip.



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

def initialize (source, *ranges, bounds: :skip, fill_value: nil)
  if source.is_a?(CArray) && source.obj_type == CA_OBJ_WINDOW
    # Backward-compat: read geometry from a CAWindow view built by #window.
    # start[i] = lo, dim[i] (window width) = w, so hi = lo + w - 1.
    win     = source
    @source = win.parent
    widths  = win.count
    @ranges = win.start.each_with_index.map { |lo, i| lo..(lo + widths[i] - 1) }
    # The legacy #window default is FILL (constant), whose value is the
    # view's fill_value; map that to a :constant margin.
    @bounds     = :constant
    @fill_value = win.fill_value
  else
    @source     = source
    @ranges     = ranges.flatten(0)
    @bounds     = bounds
    @fill_value = fill_value
  end

  unless @ranges.size == @source.ndim
    raise ArgumentError,
          "windows: expected #{@source.ndim} ranges (one per axis), " \
          "got #{@ranges.size}"
  end

  @sndim  = @source.ndim
  @widths = @ranges.map { |r| r.end - r.begin + 1 }
  @lefts  = @ranges.map { |r| [0, -r.begin].max }   # left margin per axis
  @rights = @ranges.map { |r| [0,  r.end ].max }    # right margin per axis

  # A constant fill_value: overrides :skip (constant margin escape hatch).
  @bounds = :constant if @fill_value != nil && @bounds == :skip

  # Trailing window axes of the sliding_windows view: [ndim .. 2*ndim-1].
  @window_axes = (@sndim...(2 * @sndim)).to_a

  # Output iteration space (reference-shaped, except :truncate which shrinks).
  rshape = @source.shape
  if @bounds == :truncate
    @shape = @sndim.times.map { |i| rshape[i] - @widths[i] + 1 }
  else
    @shape = rshape.dup
  end
  @ndim = @shape.size
  self
end

Instance Attribute Details

#boundsSymbol (readonly)

Returns the boundary policy symbol.

Returns:

  • (Symbol)


138
139
140
# File 'lib/carray/window_iterator.rb', line 138

def bounds
  @bounds
end

#sourceCArray (readonly)

Returns the array being rolled over.

Returns:



133
134
135
# File 'lib/carray/window_iterator.rb', line 133

def source
  @source
end

Instance Method Details

#convolve(kernel, min_count: nil, fill_value: nil) ⇒ CArray

Rolling convolution out[i] = Σ_j a[i-j]·k[j] (true convolution: the kernel is flipped on every window axis). Equals #correlate for a symmetric kernel.

Parameters:

  • kernel (CArray)

    weights shaped like a single window.

Returns:



410
411
412
# File 'lib/carray/window_iterator.rb', line 410

def convolve (kernel, min_count: nil, fill_value: nil)
  correlate(reverse_all_axes(kernel), min_count: min_count, fill_value: fill_value)
end

#correlate(kernel, min_count: nil, fill_value: nil) ⇒ CArray

Rolling cross-correlation out[i] = Σ_j a[i+j]·k[j] (kernel not flipped). kernel has the shape of one window (w_1 × ... × w_n).

Parameters:

  • kernel (CArray)

    weights shaped like a single window.

Returns:



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/carray/window_iterator.rb', line 383

def correlate (kernel, min_count: nil, fill_value: nil)
  unless kernel.shape == @widths
    raise ArgumentError,
          "correlate: kernel shape #{kernel.shape.inspect} != " \
          "window shape #{@widths.inspect}"
  end
  sv = sliding_view
  # Explicit broadcast of the kernel over the anchor axes: reshape to
  # 1 on every anchor axis, kernel width on every window axis (CArray forbids
  # implicit cross-ndim broadcast, so the shape is made explicit).
  kshape = ([1] * @sndim) + @widths
  # The product routes operand promotion through the single-source binop
  # coercion (result_type), so a float kernel over an int source promotes to
  # float instead of truncating the weights. Do not coerce the kernel here.
  prod   = sv * kernel.reshape(*kshape)
  kw = {}
  kw[:min_count]  = min_count  unless min_count.nil?
  kw[:fill_value] = fill_value unless fill_value.nil?
  prod.sum(axis: @window_axes, **kw)
end

#count(v = <none>) ⇒ CArray

Rolling count over the window. No argument counts present (non-masked) cells (the effective tap count, which drops near a :skip edge); count(UNDEF) counts masked cells; count(v) counts cells equal to v.

Returns:



334
335
336
337
338
339
# File 'lib/carray/window_iterator.rb', line 334

def count (*args)
  return count_not_masked if args.empty?
  # The sliding_windows view is a CAStride, so its #count is not shadowed;
  # dispatch CArray#count explicitly anyway, matching the family regularity.
  CArray.instance_method(:count).bind_call(sliding_view, *args, axis: @window_axes)
end

#count_maskedCArray

Rolling count of masked cells.

Returns:



352
353
354
# File 'lib/carray/window_iterator.rb', line 352

def count_masked
  sliding_view.count_masked(axis: @window_axes)
end

#count_not_maskedCArray

Rolling count of present (non-masked) cells -- the denominator of a renormalizing convolution.

Returns:



345
346
347
# File 'lib/carray/window_iterator.rb', line 345

def count_not_masked
  sliding_view.count_not_masked(axis: @window_axes)
end

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

Yields each anchor's window as a CArray. Without a block, returns an Enumerator. Per-window materialize, slow; use a named reduction or #convolve for speed.

Yield Parameters:

Returns:

  • (Enumerator, self)


540
541
542
543
544
545
546
# File 'lib/carray/window_iterator.rb', line 540

def each
  return to_enum(:each) unless block_given?
  sv   = sliding_view
  nils = Array.new(@sndim, nil)      # full window on the trailing axes
  each_anchor_index { |idx| yield sv[*idx, *nils] }
  self
end

#elementsCArray

Window cell count (structural, mask-independent): the constant window size Π w_i, shaped like the output.

Returns:



360
361
362
363
364
365
366
367
# File 'lib/carray/window_iterator.rb', line 360

def elements
  sz = @widths.inject(1) { |p, w| p * w }
  # count_not_masked gives the correct output shape (and is not shadowed);
  # overwrite with the constant window size.
  out = sliding_view.count_not_masked(axis: @window_axes)
  out[] = sz
  out
end

#mapObject

Not supported for a window iterator: overlapping windows share cells, so an element-wise transform has no well-defined scatter-back. Raises NotImplementedError; use #reduce for a custom per-window fold.

Raises:

  • (NotImplementedError)

Raises:

  • (NotImplementedError)


583
584
585
586
587
588
# File 'lib/carray/window_iterator.rb', line 583

def map (*)
  raise NotImplementedError,
        "#{self.class} has no map: overlapping windows share cells, so an " \
        "element-wise scatter-back is ill-defined; use reduce for a custom " \
        "per-window fold."
end

#max_addrCArray

Rolling flat source address of the window maximum. See #min_addr.

Returns:

  • (CArray)

    reference-shaped (or shrunk, for :truncate)



289
# File 'lib/carray/window_iterator.rb', line 289

def max_addr; window_winner_addr(:max_index); end

#medianCArray

Rolling median. Requires an unmasked margin (bounds: :nearest or :truncate); with the default :skip it raises.

Returns:



443
444
445
# File 'lib/carray/window_iterator.rb', line 443

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

#min_addrCArray

Rolling flat SOURCE address of the window minimum — which source cell holds it, so source.reshape(source.elements)[sw.min_addr] are the window minima. Unlike min_index (the position within the window) this indexes back into the original array. The winner's source cell is the anchor plus its window offset; with bounds: :nearest a winning margin cell resolves to the edge source cell it replicates, and with bounds: :constant (or fill_value:) a winning margin cell has no source address and is a masked result.

Returns:

  • (CArray)

    reference-shaped (or shrunk, for :truncate)



284
# File 'lib/carray/window_iterator.rb', line 284

def min_addr; window_winner_addr(:min_index); end

#cumsumObject #cumprodObject #cummaxObject #cumminObject #cumcountObject

Overloads:

  • #cumcountObject

    Not supported for a window iterator: a segment scan writes a per-cell running statistic, which is single-valued only when each cell belongs to exactly one piece. Overlapping windows put a cell in many windows, so there is no single running value. Raises NotImplementedError, exactly as #map / #sort_addr do (min / max reductions stay available: a single winner is well-defined).

    Raises:

    • (NotImplementedError)


265
266
267
268
269
270
271
272
273
# File 'lib/carray/window_iterator.rb', line 265

[:sum, :prod, :mean, :min, :max, :variance, :stddev, :all, :any,
 :variancep, :stddevp, :minmax, :min_index, :max_index].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?
    sliding_view.send(op, axis: @window_axes, **kw)
  end
end

#percentile(*pers) ⇒ CArray+

Rolling percentile(s). One argument returns one CArray, several return an array of CArrays (as CArray#percentile). Requires an unmasked margin.

Returns:



452
453
454
# File 'lib/carray/window_iterator.rb', line 452

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

#quantileArray<CArray>

Rolling five-number summary [min, Q1, median, Q3, max] (five CArrays), as CArray#quantile. Requires an unmasked margin.

Returns:



460
461
462
# File 'lib/carray/window_iterator.rb', line 460

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

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

Overloads:

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

    Custom rolling reduction: the block receives each window (a CArray) and returns one value per anchor. The escape hatch for statistics not in the named surface.

    Yield Parameters:

    Returns:

    • (CArray)

      reference-shaped (or shrunk, for :truncate)

  • #reduce(init) ⇒ CArray

    Per-window fiber fold: each window's cells are folded element by element starting from init.

    Parameters:

    • init (Object)

      initial accumulator.

    Returns:

Raises:

  • (LocalJumpError)


559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/carray/window_iterator.rb', line 559

def reduce (*args, data_type: nil, &blk)
  raise LocalJumpError, "no block given (yield)" unless blk
  dt   = data_type || CA_OBJECT
  out  = CArray.new(dt, @shape)
  sv   = sliding_view
  nils = Array.new(@sndim, nil)      # full window on the trailing axes
  if args.empty?
    each_anchor_index { |idx| out[*idx] = blk.call(sv[*idx, *nils]) }
  else
    init = args[0]
    each_anchor_index do |idx|
      acc = init
      sv[*idx, *nils].each { |e| acc = blk.call(acc, e) }
      out[*idx] = acc
    end
  end
  out
end

#sliding_viewCArray

Returns the sliding_windows view feeding the reductions. For :truncate this is the source's own view (zero-copy); otherwise it is the view over the padded entity.

Returns:



151
152
153
# File 'lib/carray/window_iterator.rb', line 151

def sliding_view
  @sliding_view ||= padded_entity.sliding_windows(*@widths)
end

#sort_addrObject

Not supported for a window iterator: a window's boundary cells are padding with no source address, and overlapping windows share cells, so a per-window sort returning source flat addresses is ill-defined. Raises NotImplementedError. (min_addr / max_addr are fine: the single winning cell of a window is a real source cell.)

Raises:

  • (NotImplementedError)

Raises:

  • (NotImplementedError)


597
598
599
600
601
602
# File 'lib/carray/window_iterator.rb', line 597

def sort_addr (*)
  raise NotImplementedError,
        "#{self.class} has no sort_addr: padded boundary cells have no " \
        "source address and overlapping windows share cells, so a per-window " \
        "sort of source addresses is ill-defined."
end

#wmean(weights) ⇒ CArray

Rolling weighted mean, weights shaped like a single window.

Returns:



502
503
504
# File 'lib/carray/window_iterator.rb', line 502

def wmean (weights)
  weighted(weights) { |sv, w, axis| sv.wmean(w, axis: axis) }
end

#wsum(weights) ⇒ CArray

Rolling weighted sum, weights shaped like a single window.

Returns:



495
496
497
# File 'lib/carray/window_iterator.rb', line 495

def wsum (weights)
  weighted(weights) { |sv, w, axis| sv.wsum(w, axis: axis) }
end