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
129
130
131
132
133
134
135
136
137
# 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 keeps
  # only the anchors whose window lies wholly inside the source), and where
  # each axis's first window starts in the buffer the windows are read from.
  # A window that does not cover its own anchor -- `windows(1..2)`, the two
  # cells after this one -- starts further along than the margin allows for,
  # which is what @origins carries.
  rshape = @source.shape
  if @bounds == :truncate
    first  = @ranges.map { |r| [0, -r.begin].max }
    last   = @sndim.times.map { |i| [rshape[i] - 1, rshape[i] - 1 - @ranges[i].end].min }
    @shape = @sndim.times.map { |i| [last[i] - first[i] + 1, 0].max }
    @origins = @ranges.map { |r| [r.begin, 0].max }
  else
    @shape = rshape.dup
    @origins = @sndim.times.map { |i| @lefts[i] + @ranges[i].begin }
  end
  @ndim = @shape.size
  self
end

Instance Attribute Details

#boundsSymbol (readonly)

Returns the boundary policy symbol.

Returns:

  • (Symbol)


147
148
149
# File 'lib/carray/window_iterator.rb', line 147

def bounds
  @bounds
end

#sourceCArray (readonly)

Returns the array being rolled over.

Returns:



142
143
144
# File 'lib/carray/window_iterator.rb', line 142

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:



682
683
684
# File 'lib/carray/window_iterator.rb', line 682

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:



655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
# File 'lib/carray/window_iterator.rb', line 655

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:



606
607
608
609
610
611
# File 'lib/carray/window_iterator.rb', line 606

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:



624
625
626
# File 'lib/carray/window_iterator.rb', line 624

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:



617
618
619
# File 'lib/carray/window_iterator.rb', line 617

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)


812
813
814
815
816
817
818
# File 'lib/carray/window_iterator.rb', line 812

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:



632
633
634
635
636
637
638
639
# File 'lib/carray/window_iterator.rb', line 632

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)


855
856
857
858
859
860
# File 'lib/carray/window_iterator.rb', line 855

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)



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

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:



715
716
717
# File 'lib/carray/window_iterator.rb', line 715

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)



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

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)


314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/carray/window_iterator.rb', line 314

[:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev, :all, :any,
 :variancep, :stddevp, :minmax, :min_index, :max_index].each do |op|
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
    def #{op} (min_count: nil, fill_value: nil)
      folded = fold_by_offset(:#{op}, min_count, fill_value)
      return folded unless folded.nil?
      kw = {}
      kw[:min_count]  = min_count  unless min_count.nil?
      kw[:fill_value] = fill_value unless fill_value.nil?
      sliding_view.#{op}(axis: @window_axes, **kw)
    end
  RUBY
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:



724
725
726
# File 'lib/carray/window_iterator.rb', line 724

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:



732
733
734
# File 'lib/carray/window_iterator.rb', line 732

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)


831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
# File 'lib/carray/window_iterator.rb', line 831

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:



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

def sliding_view
  @sliding_view ||= anchored_buffer.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)


869
870
871
872
873
874
# File 'lib/carray/window_iterator.rb', line 869

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:



774
775
776
# File 'lib/carray/window_iterator.rb', line 774

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:



767
768
769
# File 'lib/carray/window_iterator.rb', line 767

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