convolver 2.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ // ext/convolver/correlate_raw.h
2
+
3
+ ////////////////////////////////////////////////////////////////////////////////////////////////
4
+ //
5
+ // Declarations of narray helper functions
6
+ //
7
+
8
+ #ifndef CORRELATE_RAW_H
9
+ #define CORRELATE_RAW_H
10
+
11
+ #ifndef CONVOLVER_RAW_CONFIG
12
+ #define CONVOLVER_RAW_CONFIG
13
+ #include <ruby.h>
14
+ #if defined(__SSE__)
15
+ #include <xmmintrin.h>
16
+ #define CONVOLVER_USE_SSE 1
17
+ #else
18
+ #define CONVOLVER_USE_SSE 0
19
+ #endif
20
+ #define LARGEST_RANK 16
21
+ #endif
22
+
23
+ void correlate_raw(
24
+ int in_rank, const size_t *in_shape, const float *in_ptr,
25
+ int kernel_rank, const size_t *kernel_shape, const float *kernel_ptr,
26
+ int out_rank, const size_t *out_shape, float *out_ptr );
27
+
28
+ #endif
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Convolver
4
+ # Checks individual allocations against native size and tagged-stride limits.
5
+ # @private
6
+ class BufferLimits
7
+ SIZE_MAX = (1 << ([0].pack('J').bytesize * 8)) - 1
8
+ # Numo tags signed strides with a left shift: floor(SSIZE_MAX / 2).
9
+ BYTE_MAX = SIZE_MAX / 4
10
+ INT_MAX = (1 << (([0].pack('i').bytesize * 8) - 1)) - 1
11
+ INDEX_BYTES = [0].pack('J').bytesize
12
+
13
+ attr_reader :size_max, :byte_max, :int_max
14
+
15
+ def initialize(size_max: SIZE_MAX, byte_max: BYTE_MAX, int_max: INT_MAX)
16
+ @size_max = size_max
17
+ @byte_max = byte_max
18
+ @int_max = int_max
19
+ end
20
+
21
+ def buffer!(count, width, description, error: RangeError)
22
+ return if count <= byte_max / width
23
+
24
+ raise error, "#{description} exceeds native byte/stride limit"
25
+ end
26
+
27
+ def product!(shape, description, error: RangeError)
28
+ shape.reduce(1) do |product, size|
29
+ raise error, "#{description} exceeds native element limit" if size > size_max / product
30
+
31
+ product * size
32
+ end
33
+ end
34
+
35
+ NATIVE = new.freeze
36
+ end
37
+
38
+ private_constant :BufferLimits
39
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'convolver/fft_limits'
4
+
5
+ module Convolver
6
+ # Preflights casts, extension, indexing and direct-path cache allocations.
7
+ # @private
8
+ class BufferPreparation
9
+ def initialize(plan, signal_shape, kernel_shape, limits: BufferLimits::NATIVE)
10
+ @plan = plan
11
+ @signal_size = limits.product!(signal_shape, 'signal size')
12
+ @kernel_size = limits.product!(kernel_shape, 'kernel size')
13
+ @limits = limits
14
+ @scalar = signal_shape.empty?
15
+ end
16
+
17
+ def basic!
18
+ common!
19
+ extension!
20
+ limits.buffer!(kernel_size, BufferLimits::INDEX_BYTES, 'direct kernel offset cache')
21
+ end
22
+
23
+ def fft!
24
+ common!
25
+ return if @scalar
26
+ return extension! unless plan.wrap?
27
+
28
+ limits.buffer!(signal_size, 8, 'circular signal preparation', error: FftUnavailable)
29
+ limits.buffer!(kernel_size, 8, 'circular kernel preparation', error: FftUnavailable)
30
+ limits.buffer!(kernel_size, BufferLimits::INDEX_BYTES, 'circular kernel index buffer', error: FftUnavailable)
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :plan, :signal_size, :kernel_size, :limits
36
+
37
+ def common!
38
+ limits.buffer!(plan.result_size, 4, 'result buffer')
39
+ limits.buffer!(signal_size, 4, 'signal conversion buffer')
40
+ limits.buffer!(kernel_size, 4, 'kernel conversion buffer')
41
+ end
42
+
43
+ def extension!
44
+ return if plan.valid?
45
+
46
+ limits.buffer!(plan.extended_size, 4, 'extended signal buffer')
47
+ return if plan.boundary == :constant
48
+
49
+ plan.extended_shape.each do |length|
50
+ limits.buffer!(length, BufferLimits::INDEX_BYTES, 'boundary index buffer')
51
+ end
52
+ end
53
+ end
54
+
55
+ private_constant :BufferPreparation
56
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Convolver
4
+ # Calculates same-sized periodic operations through PocketFFT transforms.
5
+ # @private
6
+ class CircularFftOperation
7
+ def initialize(operation, signal, kernel, plan)
8
+ @real_axis = plan.fft_buffers.real_axis
9
+ @operation = operation
10
+ @signal = Numo::DFloat.cast(signal)
11
+ @kernel = fold_kernel(Numo::DFloat.cast(kernel), signal.shape, plan.anchors)
12
+ end
13
+
14
+ def call
15
+ return complex_product unless real_axis
16
+
17
+ real_product(real_axis)
18
+ end
19
+
20
+ def self.real_axis(shape)
21
+ return nil if shape.empty?
22
+
23
+ final_axis = shape.length - 1
24
+ return final_axis if shape[final_axis].even?
25
+
26
+ shape.each_index.select { |axis| shape[axis].even? }.max_by { |axis| shape[axis] }
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :operation, :signal, :kernel, :real_axis
32
+
33
+ def real_product(real_axis)
34
+ final_axis = signal.ndim - 1
35
+ prepared_signal = prepare_real_operand(signal, real_axis, final_axis)
36
+ prepared_kernel = prepare_real_operand(kernel, real_axis, final_axis)
37
+ result = real_spectrum_product(prepared_signal, prepared_kernel)
38
+ result = result.swapaxes(real_axis, final_axis) unless real_axis == final_axis
39
+ Numo::SFloat.cast(result)
40
+ end
41
+
42
+ def prepare_real_operand(value, real_axis, final_axis)
43
+ real_axis == final_axis ? value : value.swapaxes(real_axis, final_axis)
44
+ end
45
+
46
+ def real_spectrum_product(signal_value, kernel_value)
47
+ signal_spectrum = Numo::Pocketfft.rfftn(signal_value)
48
+ kernel_spectrum = Numo::Pocketfft.rfftn(kernel_value)
49
+ kernel_spectrum.inplace.conj if operation == :correlation
50
+ Numo::Pocketfft.irfftn(signal_spectrum.inplace * kernel_spectrum)
51
+ end
52
+
53
+ def complex_product
54
+ signal_spectrum = Numo::Pocketfft.fftn(signal)
55
+ kernel_spectrum = Numo::Pocketfft.fftn(kernel)
56
+ kernel_spectrum.inplace.conj if operation == :correlation
57
+ result = Numo::Pocketfft.ifftn(signal_spectrum.inplace * kernel_spectrum).real
58
+ Numo::SFloat.cast(result)
59
+ end
60
+
61
+ def fold_kernel(source, signal_shape, anchors)
62
+ folded = Numo::DFloat.zeros(*signal_shape)
63
+ source.flatten.each_with_index do |value, flat_index|
64
+ target = folded_target(flat_index, source.shape, signal_shape, anchors)
65
+ folded[*target] = folded[*target] + value
66
+ end
67
+ folded
68
+ end
69
+
70
+ def folded_target(flat_index, kernel_shape, signal_shape, anchors)
71
+ remainder = flat_index
72
+ Array.new(kernel_shape.length).tap do |target|
73
+ (kernel_shape.length - 1).downto(0) do |axis|
74
+ coordinate = remainder % kernel_shape[axis]
75
+ remainder /= kernel_shape[axis]
76
+ target[axis] = (coordinate - anchors[axis]) % signal_shape[axis]
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ private_constant :CircularFftOperation
83
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'convolver/fft_limits'
4
+ require 'convolver/buffer_preparation'
5
+
6
+ module Convolver
7
+ # Shares allocation-free FFT dimensions between execution and estimation.
8
+ # @private
9
+ class FftBufferPlan
10
+ attr_reader :transform_shape, :real_axis, :transform_size, :spectrum_size
11
+
12
+ def initialize(plan, signal_shape, kernel_shape, limits: BufferLimits::NATIVE)
13
+ BufferPreparation.new(plan, signal_shape, kernel_shape, limits:).fft!
14
+ @real_axis = plan.wrap? ? CircularFftOperation.real_axis(signal_shape) : signal_shape.length - 1
15
+ @transform_shape = select_shape(plan, signal_shape, kernel_shape, FftLimits.new(limits))
16
+ @transform_size = transform_shape.reduce(1, :*)
17
+ @spectrum_size = spectrum_elements
18
+ end
19
+
20
+ private
21
+
22
+ def select_shape(plan, signal_shape, kernel_shape, limits)
23
+ return [].freeze if signal_shape.empty?
24
+ return plan.linear_fft_shape(kernel_shape, limits:) unless plan.wrap?
25
+
26
+ limits.validate!(signal_shape, real_axis:)
27
+ signal_shape.dup.freeze
28
+ end
29
+
30
+ def spectrum_elements
31
+ return transform_size if transform_shape.empty? || real_axis.nil?
32
+
33
+ length = transform_shape[real_axis]
34
+ transform_size / length * ((length / 2) + 1)
35
+ end
36
+ end
37
+
38
+ private_constant :FftBufferPlan
39
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Convolver
4
+ # Estimates the selected real or complex PocketFFT implementation cost.
5
+ # @private
6
+ class FftEstimator
7
+ REAL_FFT_COST = { 1 => 1.0e-9, 2 => 9.0e-10, 3 => 1.45e-9 }.freeze
8
+ COMPLEX_FFT_COST = { 1 => 2.4e-9, 2 => 2.4e-9, 3 => 2.8e-9 }.freeze
9
+ FIXED_COST = 1.5e-4
10
+ PREPARATION_COST = 1.0e-9
11
+ AXIS_MOVE_COST = 1.6e-9
12
+ CORRELATION_COST = 2.0e-10
13
+
14
+ def initialize(operation, signal, kernel, plan)
15
+ @operation = operation
16
+ @signal = signal
17
+ @kernel = kernel
18
+ @plan = plan
19
+ @buffers = plan.fft_buffers
20
+ end
21
+
22
+ def call
23
+ transform_size, spectrum_size, coefficient, moved_size = dimensions
24
+ transform_cost = coefficient * transform_size * Math.log([transform_size, 2].max)
25
+ preparation_cost = PREPARATION_COST * preparation_size(transform_size)
26
+ axis_move_cost = AXIS_MOVE_COST * moved_size
27
+ correlation_cost = operation == :correlation ? CORRELATION_COST * spectrum_size : 0.0
28
+ FIXED_COST + transform_cost + preparation_cost + axis_move_cost + correlation_cost
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :operation, :signal, :kernel, :plan, :buffers
34
+
35
+ def dimensions
36
+ return linear_dimensions unless plan.wrap?
37
+
38
+ circular_dimensions
39
+ end
40
+
41
+ def circular_dimensions
42
+ real_axis = buffers.real_axis
43
+ [buffers.transform_size, buffers.spectrum_size, transform_coefficient(real_axis), moved_size(real_axis)]
44
+ end
45
+
46
+ def linear_dimensions
47
+ [buffers.transform_size, buffers.spectrum_size,
48
+ cost_for(REAL_FFT_COST), 0]
49
+ end
50
+
51
+ def preparation_size(transform_size)
52
+ extension_size = plan.valid? || plan.wrap? ? 0 : plan.extended_size
53
+ working_size = plan.wrap? ? signal.size + kernel.size : 2 * transform_size
54
+ extension_size + working_size
55
+ end
56
+
57
+ def cost_for(costs)
58
+ costs.fetch(signal.ndim, costs.values.last)
59
+ end
60
+
61
+ def transform_coefficient(real_axis)
62
+ cost_for(real_axis ? REAL_FFT_COST : COMPLEX_FFT_COST)
63
+ end
64
+
65
+ def moved_size(real_axis)
66
+ real_axis && real_axis != signal.ndim - 1 ? 3 * signal.size : 0
67
+ end
68
+ end
69
+
70
+ private_constant :FftEstimator
71
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'convolver/buffer_limits'
4
+
5
+ # Internal allocation and arithmetic checks for the PocketFFT paths.
6
+ module Convolver
7
+ # Only FFT representability failures permit automatic direct fallback.
8
+ # @private
9
+ class FftUnavailable < RangeError; end
10
+
11
+ # Bounds PocketFFT buffers and signed arithmetic without constructing plans.
12
+ # @private
13
+ class FftLimits
14
+ def initialize(buffers = BufferLimits::NATIVE)
15
+ @buffers = buffers
16
+ end
17
+
18
+ def validate!(shape, real_axis:)
19
+ total = buffers.product!(shape, 'FFT size', error: FftUnavailable)
20
+ validate_buffers!(shape, total, real_axis)
21
+ # The wrapper casts total to int before dividing it by the axis length.
22
+ integer!(total, buffers.int_max, 'FFT total size')
23
+ # Real forward computes int z_step = (z / 2 + 1) * 2.
24
+ integer!(shape[real_axis], buffers.int_max - 2, 'FFT real stride') if real_axis
25
+ validate_axes!(shape)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :buffers
31
+
32
+ def validate_axes!(shape)
33
+ # FFTPACK twiddle counts are <= 2n complex / 3n real. Bluestein m < 4n,
34
+ # so each variable allocation is <= 128n bytes, including inner twiddles.
35
+ # The 16n int bound also protects signed intermediates at the inner m.
36
+ shape.each do |length|
37
+ integer!(length, buffers.int_max / 16, 'FFT axis arithmetic')
38
+ buffer!(length, 128, 'FFT axis work buffer')
39
+ end
40
+ end
41
+
42
+ def validate_buffers!(shape, total, real_axis)
43
+ buffer!(total, 8, 'FFT spatial buffer')
44
+ return buffer!(total, 16, 'FFT complex buffer') unless real_axis
45
+
46
+ spectrum = total / shape[real_axis] * ((shape[real_axis] / 2) + 1)
47
+ buffer!(spectrum, 16, 'FFT spectrum buffer')
48
+ buffer!(total, 16, 'FFT inverse staging buffer')
49
+ buffer!(total, 8, 'FFT inverse output buffer')
50
+ end
51
+
52
+ def buffer!(count, width, description)
53
+ buffers.buffer!(count, width, description, error: FftUnavailable)
54
+ end
55
+
56
+ def integer!(value, maximum, description)
57
+ return if value <= maximum
58
+
59
+ raise FftUnavailable, "#{description} exceeds native integer limit"
60
+ end
61
+ end
62
+
63
+ private_constant :FftUnavailable, :FftLimits
64
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Convolver
4
+ # Calculates a valid linear operation through real PocketFFT transforms.
5
+ # @private
6
+ class LinearFftOperation
7
+ def initialize(operation, signal, kernel, plan)
8
+ @operation = operation
9
+ @signal = signal
10
+ @kernel = kernel
11
+ @transform_shape = plan.fft_buffers.transform_shape
12
+ end
13
+
14
+ def call
15
+ signal_padded = zero_padded(signal)
16
+ kernel_padded = zero_padded(kernel)
17
+ full_result = real_spectrum_product(signal_padded, kernel_padded)
18
+ Numo::SFloat.cast(full_result[*valid_ranges])
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :operation, :signal, :kernel, :transform_shape
24
+
25
+ def zero_padded(value)
26
+ Numo::DFloat.zeros(*transform_shape).tap do |padded|
27
+ padded[*value.shape.map { |size| 0...size }] = value
28
+ end
29
+ end
30
+
31
+ def real_spectrum_product(signal_value, kernel_value)
32
+ signal_spectrum = Numo::Pocketfft.rfftn(signal_value)
33
+ kernel_spectrum = Numo::Pocketfft.rfftn(kernel_value)
34
+ kernel_spectrum.inplace.conj if operation == :correlation
35
+ Numo::Pocketfft.irfftn(signal_spectrum.inplace * kernel_spectrum)
36
+ end
37
+
38
+ def valid_ranges
39
+ signal.shape.zip(kernel.shape).map do |signal_size, kernel_size|
40
+ if operation == :correlation
41
+ 0...(signal_size - kernel_size + 1)
42
+ else
43
+ (kernel_size - 1)...signal_size
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ private_constant :LinearFftOperation
50
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Convolver
4
+ # Coordinates validation, implementation selection, and one operation family.
5
+ # @private
6
+ class OperationExecution
7
+ DIRECT_OPERATION_COST = { 1 => 6.8e-10, 2 => 5.9e-10, 3 => 4.5e-10 }.freeze
8
+ EXTENSION_COST = 1.0e-9
9
+ FFT_SPEEDUP_MARGIN = 0.8
10
+
11
+ METHODS = {
12
+ correlation: {
13
+ basic: :correlate_basic,
14
+ fft: :correlate_fft,
15
+ predict_basic: :predict_correlate_basic_time,
16
+ predict_fft: :predict_correlate_fft_time
17
+ },
18
+ convolution: {
19
+ basic: :convolve_basic,
20
+ fft: :convolve_fft,
21
+ predict_basic: :predict_convolve_basic_time,
22
+ predict_fft: :predict_convolve_fft_time
23
+ }
24
+ }.freeze
25
+
26
+ def initialize(operation, signal, kernel, mode:, boundary:, fill_value:, origin:)
27
+ @operation = operation
28
+ @signal = signal
29
+ @kernel = kernel
30
+ @mode = mode
31
+ @boundary = boundary
32
+ @fill_value = fill_value
33
+ @origin = origin
34
+ end
35
+
36
+ def automatic(receiver)
37
+ return invoke(receiver, :basic) if plan.extended_size < 1000
38
+
39
+ direct_time = invoke(receiver, :predict_basic)
40
+ return invoke(receiver, :fft) if automatic_fft_time(receiver) < FFT_SPEEDUP_MARGIN * direct_time
41
+
42
+ invoke(receiver, :basic)
43
+ end
44
+
45
+ def basic
46
+ plan.validate_basic!
47
+ extended_signal = plan.extend_signal(signal)
48
+ return Convolver.send(:correlate_basic_valid, extended_signal, kernel) if operation == :correlation
49
+
50
+ Convolver.send(:convolve_basic_valid, extended_signal, kernel)
51
+ end
52
+
53
+ def fft
54
+ plan.fft_buffers
55
+ return Numo::SFloat.cast(signal) * Numo::SFloat.cast(kernel) if signal.ndim.zero?
56
+
57
+ fft_operation.call
58
+ end
59
+
60
+ def fft_time
61
+ FftEstimator.new(operation, signal, kernel, plan).call
62
+ end
63
+
64
+ def basic_time
65
+ plan.validate_basic!
66
+ calculation_cost = direct_operation_cost * plan.result_size * kernel.size
67
+ extension_cost = plan.valid? ? 0.0 : EXTENSION_COST * plan.extended_size
68
+ calculation_cost + extension_cost
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :operation, :signal, :kernel, :mode, :boundary, :fill_value, :origin
74
+
75
+ def plan
76
+ @plan ||= OperationPlan.new(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
77
+ end
78
+
79
+ def invoke(receiver, method)
80
+ receiver.public_send(METHODS.fetch(operation).fetch(method), signal, kernel, **public_options)
81
+ end
82
+
83
+ def public_options
84
+ options = { mode:, boundary:, origin: }
85
+ options[:fill_value] = fill_value unless fill_value.equal?(UNSPECIFIED_FILL)
86
+ options
87
+ end
88
+
89
+ def fft_operation
90
+ return CircularFftOperation.new(operation, signal, kernel, plan) if plan.wrap?
91
+
92
+ LinearFftOperation.new(operation, plan.extend_signal(signal), kernel, plan)
93
+ end
94
+
95
+ def automatic_fft_time(receiver)
96
+ invoke(receiver, :predict_fft)
97
+ rescue FftUnavailable
98
+ Float::INFINITY
99
+ end
100
+
101
+ def direct_operation_cost
102
+ DIRECT_OPERATION_COST.fetch(signal.ndim, DIRECT_OPERATION_COST.values.last)
103
+ end
104
+ end
105
+
106
+ private_constant :OperationExecution
107
+ end
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Convolver
4
- # Validates and normalizes the options for a correlation operation.
4
+ # Validates and normalizes one convolution or correlation operation.
5
+ # @private
5
6
  class OperationOptions
7
+ OPERATIONS = %i[convolution correlation].freeze
6
8
  MODES = %i[valid same full].freeze
7
9
  BOUNDARIES = %i[constant nearest reflect mirror wrap].freeze
8
10
 
9
- attr_reader :mode, :boundary, :fill_value, :origins, :anchors
11
+ attr_reader :operation, :mode, :boundary, :fill_value, :origins, :anchors
10
12
 
11
- def initialize(signal, kernel, mode:, boundary:, fill_value:, origin:)
13
+ def initialize(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
12
14
  validate_inputs!(signal, kernel)
13
- validate_vocabulary!(mode, boundary)
14
- assign_options(signal, kernel, mode:, boundary:, fill_value:, origin:)
15
+ validate_vocabulary!(operation, mode, boundary)
16
+ assign_options(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
15
17
  validate_combinations!(signal.shape, kernel.shape)
16
18
  end
17
19
 
@@ -26,14 +28,16 @@ module Convolver
26
28
  raise ArgumentError, "maximum supported rank is #{MAX_RANK}" if signal.ndim > MAX_RANK
27
29
  end
28
30
 
29
- def validate_vocabulary!(mode, boundary)
31
+ def validate_vocabulary!(operation, mode, boundary)
32
+ raise ArgumentError, "operation must be one of #{OPERATIONS.inspect}" unless OPERATIONS.include?(operation)
30
33
  raise ArgumentError, "mode must be one of #{MODES.inspect}" unless MODES.include?(mode)
31
34
  return if BOUNDARIES.include?(boundary)
32
35
 
33
36
  raise ArgumentError, "boundary must be one of #{BOUNDARIES.inspect}"
34
37
  end
35
38
 
36
- def assign_options(signal, kernel, mode:, boundary:, fill_value:, origin:)
39
+ def assign_options(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
40
+ @operation = operation
37
41
  @mode = mode
38
42
  @boundary = boundary
39
43
  @fill_value_given = !fill_value.equal?(UNSPECIFIED_FILL)
@@ -4,25 +4,32 @@ require 'forwardable'
4
4
  require 'convolver/operation_options'
5
5
  require 'convolver/operation_shapes'
6
6
  require 'convolver/signal_extension'
7
+ require 'convolver/fft_buffer_plan'
7
8
 
8
9
  # Internal planning and extension support for Convolver's public operations.
9
10
  module Convolver
10
11
  # Distinguishes an omitted fill_value keyword from an explicitly supplied
11
12
  # value. This lets non-constant boundaries reject even an explicit zero.
13
+ # @private
12
14
  UNSPECIFIED_FILL = Object.new.freeze
13
15
 
14
- # Validated dimensions and boundary-extension details for one correlation.
16
+ # Validated dimensions and boundary-extension details for one operation.
17
+ # @private
15
18
  class OperationPlan
16
19
  extend Forwardable
17
20
 
18
- def_delegators :options, :mode, :boundary, :fill_value, :origins, :anchors
21
+ def_delegators :options, :operation, :mode, :boundary, :fill_value, :origins, :anchors
19
22
  def_delegators :shapes, :padding_before, :padding_after, :result_shape,
20
23
  :extended_shape, :result_size, :extended_size, :linear_fft_shape,
21
- :linear_fft_size
22
-
23
- def initialize(signal, kernel, mode:, boundary:, fill_value:, origin:)
24
- @options = OperationOptions.new(signal, kernel, mode:, boundary:, fill_value:, origin:)
25
- @shapes = OperationShapes.new(signal.shape, kernel.shape, mode:, anchors: options.anchors)
24
+ :linear_fft_size, :linear_spectrum_size
25
+
26
+ def initialize(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
27
+ @options = OperationOptions.new(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
28
+ @signal_shape = signal.shape.freeze
29
+ @kernel_shape = kernel.shape.freeze
30
+ @shapes = OperationShapes.new(
31
+ signal.shape, kernel.shape, operation:, mode:, anchors: options.anchors
32
+ )
26
33
  end
27
34
 
28
35
  def valid?
@@ -39,6 +46,14 @@ module Convolver
39
46
  SignalExtension.new(shapes, boundary:, fill_value:).call(signal)
40
47
  end
41
48
 
49
+ def validate_basic!
50
+ BufferPreparation.new(self, @signal_shape, @kernel_shape).basic!
51
+ end
52
+
53
+ def fft_buffers
54
+ @fft_buffers ||= FftBufferPlan.new(self, @signal_shape, @kernel_shape)
55
+ end
56
+
42
57
  private
43
58
 
44
59
  attr_reader :options, :shapes