convolver 1.0.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +153 -36
- data/ext/convolver/convolve_raw.c +11 -9
- data/ext/convolver/convolve_raw.h +3 -0
- data/ext/convolver/convolver.c +69 -25
- data/ext/convolver/correlate_raw.c +167 -0
- data/ext/convolver/correlate_raw.h +28 -0
- data/lib/convolver/buffer_limits.rb +39 -0
- data/lib/convolver/buffer_preparation.rb +56 -0
- data/lib/convolver/circular_fft_operation.rb +83 -0
- data/lib/convolver/fft_buffer_plan.rb +39 -0
- data/lib/convolver/fft_estimator.rb +71 -0
- data/lib/convolver/fft_limits.rb +64 -0
- data/lib/convolver/linear_fft_operation.rb +50 -0
- data/lib/convolver/operation_execution.rb +107 -0
- data/lib/convolver/operation_options.rb +118 -0
- data/lib/convolver/operation_plan.rb +63 -0
- data/lib/convolver/operation_shapes.rb +129 -0
- data/lib/convolver/real_fft_shape.rb +135 -0
- data/lib/convolver/signal_extension.rb +67 -0
- data/lib/convolver/version.rb +1 -1
- data/lib/convolver.rb +102 -71
- metadata +24 -9
|
@@ -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
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Validates and normalizes one convolution or correlation operation.
|
|
5
|
+
# @private
|
|
6
|
+
class OperationOptions
|
|
7
|
+
OPERATIONS = %i[convolution correlation].freeze
|
|
8
|
+
MODES = %i[valid same full].freeze
|
|
9
|
+
BOUNDARIES = %i[constant nearest reflect mirror wrap].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :operation, :mode, :boundary, :fill_value, :origins, :anchors
|
|
12
|
+
|
|
13
|
+
def initialize(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
|
|
14
|
+
validate_inputs!(signal, kernel)
|
|
15
|
+
validate_vocabulary!(operation, mode, boundary)
|
|
16
|
+
assign_options(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
|
|
17
|
+
validate_combinations!(signal.shape, kernel.shape)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def validate_inputs!(signal, kernel)
|
|
23
|
+
unless signal.is_a?(Numo::NArray) && kernel.is_a?(Numo::NArray)
|
|
24
|
+
raise ArgumentError, 'signal and kernel must be Numo::NArray values'
|
|
25
|
+
end
|
|
26
|
+
raise ArgumentError, 'signal and kernel must not be empty' if signal.empty? || kernel.empty?
|
|
27
|
+
raise ArgumentError, 'signal and kernel must have equal rank' unless signal.ndim == kernel.ndim
|
|
28
|
+
raise ArgumentError, "maximum supported rank is #{MAX_RANK}" if signal.ndim > MAX_RANK
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_vocabulary!(operation, mode, boundary)
|
|
32
|
+
raise ArgumentError, "operation must be one of #{OPERATIONS.inspect}" unless OPERATIONS.include?(operation)
|
|
33
|
+
raise ArgumentError, "mode must be one of #{MODES.inspect}" unless MODES.include?(mode)
|
|
34
|
+
return if BOUNDARIES.include?(boundary)
|
|
35
|
+
|
|
36
|
+
raise ArgumentError, "boundary must be one of #{BOUNDARIES.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def assign_options(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
|
|
40
|
+
@operation = operation
|
|
41
|
+
@mode = mode
|
|
42
|
+
@boundary = boundary
|
|
43
|
+
@fill_value_given = !fill_value.equal?(UNSPECIFIED_FILL)
|
|
44
|
+
@fill_value = normalize_fill_value(fill_value)
|
|
45
|
+
@origins = normalize_origins(origin, signal.ndim)
|
|
46
|
+
@anchors = kernel.shape.zip(@origins).map.with_index do |(kernel_size, axis_origin), axis|
|
|
47
|
+
normalize_anchor(kernel_size, axis_origin, axis)
|
|
48
|
+
end.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def normalize_fill_value(fill_value)
|
|
52
|
+
value = fill_value.equal?(UNSPECIFIED_FILL) ? 0.0 : fill_value
|
|
53
|
+
unless value.is_a?(Numeric) && !value.is_a?(Complex)
|
|
54
|
+
raise ArgumentError, 'fill_value must be a real numeric value'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
value.to_f
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def normalize_origins(origin, rank)
|
|
61
|
+
origins = case origin
|
|
62
|
+
when Integer then Array.new(rank, origin)
|
|
63
|
+
when Array then origin.dup
|
|
64
|
+
else
|
|
65
|
+
raise ArgumentError, 'origin must be an Integer or an Array of one Integer per dimension'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unless origins.length == rank && origins.all?(Integer)
|
|
69
|
+
raise ArgumentError, 'origin must contain one Integer per dimension'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
origins.freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def normalize_anchor(kernel_size, origin, axis)
|
|
76
|
+
anchor = (kernel_size / 2) + origin
|
|
77
|
+
return anchor if anchor.between?(0, kernel_size - 1)
|
|
78
|
+
|
|
79
|
+
raise ArgumentError,
|
|
80
|
+
"origin #{origin} is out of range for kernel dimension #{axis} of size #{kernel_size}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_combinations!(signal_shape, kernel_shape)
|
|
84
|
+
validate_mode_combination!(signal_shape, kernel_shape)
|
|
85
|
+
validate_fill_combination!
|
|
86
|
+
validate_origin_combination!
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_mode_combination!(signal_shape, kernel_shape)
|
|
90
|
+
return validate_valid_options!(signal_shape, kernel_shape) if mode == :valid
|
|
91
|
+
return unless mode == :full && boundary != :constant
|
|
92
|
+
|
|
93
|
+
raise ArgumentError, 'mode: :full only supports boundary: :constant'
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validate_fill_combination!
|
|
97
|
+
return unless boundary != :constant && @fill_value_given
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, 'fill_value is only supported with boundary: :constant'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_origin_combination!
|
|
103
|
+
return if mode == :same || origins.all?(&:zero?)
|
|
104
|
+
|
|
105
|
+
raise ArgumentError, 'nonzero origin is only supported with mode: :same'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def validate_valid_options!(signal_shape, kernel_shape)
|
|
109
|
+
unless signal_shape.zip(kernel_shape).all? { |signal_size, kernel_size| signal_size >= kernel_size }
|
|
110
|
+
raise ArgumentError, 'kernel must not be larger than signal in any dimension'
|
|
111
|
+
end
|
|
112
|
+
raise ArgumentError, 'mode: :valid only supports boundary: :constant' unless boundary == :constant
|
|
113
|
+
raise ArgumentError, 'mode: :valid requires fill_value: 0' unless fill_value.zero?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private_constant :OperationOptions
|
|
118
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
require 'convolver/operation_options'
|
|
5
|
+
require 'convolver/operation_shapes'
|
|
6
|
+
require 'convolver/signal_extension'
|
|
7
|
+
require 'convolver/fft_buffer_plan'
|
|
8
|
+
|
|
9
|
+
# Internal planning and extension support for Convolver's public operations.
|
|
10
|
+
module Convolver
|
|
11
|
+
# Distinguishes an omitted fill_value keyword from an explicitly supplied
|
|
12
|
+
# value. This lets non-constant boundaries reject even an explicit zero.
|
|
13
|
+
# @private
|
|
14
|
+
UNSPECIFIED_FILL = Object.new.freeze
|
|
15
|
+
|
|
16
|
+
# Validated dimensions and boundary-extension details for one operation.
|
|
17
|
+
# @private
|
|
18
|
+
class OperationPlan
|
|
19
|
+
extend Forwardable
|
|
20
|
+
|
|
21
|
+
def_delegators :options, :operation, :mode, :boundary, :fill_value, :origins, :anchors
|
|
22
|
+
def_delegators :shapes, :padding_before, :padding_after, :result_shape,
|
|
23
|
+
:extended_shape, :result_size, :extended_size, :linear_fft_shape,
|
|
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
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def valid?
|
|
36
|
+
mode == :valid
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def wrap?
|
|
40
|
+
mode == :same && boundary == :wrap
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def extend_signal(signal)
|
|
44
|
+
return signal if valid?
|
|
45
|
+
|
|
46
|
+
SignalExtension.new(shapes, boundary:, fill_value:).call(signal)
|
|
47
|
+
end
|
|
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
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
attr_reader :options, :shapes
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private_constant :OperationPlan, :UNSPECIFIED_FILL
|
|
63
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'convolver/real_fft_shape'
|
|
4
|
+
require 'convolver/fft_limits'
|
|
5
|
+
|
|
6
|
+
module Convolver
|
|
7
|
+
# Calculates and validates output, extension, and FFT dimensions.
|
|
8
|
+
# @private
|
|
9
|
+
class OperationShapes
|
|
10
|
+
SIZE_MAX = (1 << ([0].pack('J').bytesize * 8)) - 1
|
|
11
|
+
|
|
12
|
+
attr_reader :padding_before, :padding_after, :result_shape,
|
|
13
|
+
:extended_shape, :result_size, :extended_size
|
|
14
|
+
|
|
15
|
+
def initialize(signal_shape, kernel_shape, operation:, mode:, anchors:)
|
|
16
|
+
@operation = operation
|
|
17
|
+
@mode = mode
|
|
18
|
+
calculate_shapes(signal_shape, kernel_shape, anchors)
|
|
19
|
+
validate_sizes!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def linear_fft_shape(kernel_shape, limits: FftLimits.new)
|
|
23
|
+
return [].freeze if kernel_shape.empty?
|
|
24
|
+
|
|
25
|
+
RealFftShape.new(linear_result_shape(kernel_shape), size_max: SIZE_MAX, limits:).call
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def linear_fft_size(kernel_shape)
|
|
29
|
+
checked_product(linear_fft_shape(kernel_shape), 'FFT size')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def linear_spectrum_size(kernel_shape)
|
|
33
|
+
shape = linear_fft_shape(kernel_shape)
|
|
34
|
+
return 1 if shape.empty?
|
|
35
|
+
|
|
36
|
+
spectrum_shape = shape.dup
|
|
37
|
+
spectrum_shape[-1] = (spectrum_shape[-1] / 2) + 1
|
|
38
|
+
checked_product(spectrum_shape, 'FFT spectrum size')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :operation, :mode
|
|
44
|
+
|
|
45
|
+
def calculate_shapes(signal_shape, kernel_shape, anchors)
|
|
46
|
+
@padding_before, @padding_after, @result_shape = shapes_for(signal_shape, kernel_shape, anchors)
|
|
47
|
+
@extended_shape = signal_shape.zip(padding_before, padding_after).map do |signal_size, before, after|
|
|
48
|
+
signal_size + before + after
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def shapes_for(signal_shape, kernel_shape, anchors)
|
|
53
|
+
case mode
|
|
54
|
+
when :valid then valid_shapes(signal_shape, kernel_shape)
|
|
55
|
+
when :same then same_shapes(signal_shape, kernel_shape, anchors)
|
|
56
|
+
when :full then full_shapes(signal_shape, kernel_shape)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def valid_shapes(signal_shape, kernel_shape)
|
|
61
|
+
result = signal_shape.zip(kernel_shape).map do |signal_size, kernel_size|
|
|
62
|
+
signal_size - kernel_size + 1
|
|
63
|
+
end
|
|
64
|
+
zeros = Array.new(signal_shape.length, 0).freeze
|
|
65
|
+
[zeros, zeros, result]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def same_shapes(signal_shape, kernel_shape, anchors)
|
|
69
|
+
correlation_before = anchors.dup
|
|
70
|
+
correlation_after = kernel_shape.zip(anchors).map { |kernel_size, anchor| kernel_size - 1 - anchor }
|
|
71
|
+
before, after = if operation == :correlation
|
|
72
|
+
[correlation_before, correlation_after]
|
|
73
|
+
else
|
|
74
|
+
[correlation_after, correlation_before]
|
|
75
|
+
end
|
|
76
|
+
before.freeze
|
|
77
|
+
after.freeze
|
|
78
|
+
[before, after, signal_shape.dup]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def full_shapes(signal_shape, kernel_shape)
|
|
82
|
+
padding = kernel_shape.map { |kernel_size| kernel_size - 1 }.freeze
|
|
83
|
+
result = signal_shape.zip(kernel_shape).map { |signal_size, kernel_size| signal_size + kernel_size - 1 }
|
|
84
|
+
[padding, padding, result]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_sizes!
|
|
88
|
+
@padding_before, @padding_after, @result_shape, @extended_shape = [
|
|
89
|
+
[padding_before, 'padding shape'],
|
|
90
|
+
[padding_after, 'padding shape'],
|
|
91
|
+
[result_shape, 'result shape'],
|
|
92
|
+
[extended_shape, 'extended signal shape']
|
|
93
|
+
].map { |shape, description| checked_shape(shape, description).freeze }
|
|
94
|
+
@result_size = checked_product(result_shape, 'result size')
|
|
95
|
+
@extended_size = checked_product(extended_shape, 'extended signal size')
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def checked_shape(shape, description)
|
|
99
|
+
return shape if shape.all? { |size| size.between?(0, SIZE_MAX) }
|
|
100
|
+
|
|
101
|
+
raise RangeError, "#{description} exceeds native implementation limit"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def checked_product(shape, description)
|
|
105
|
+
shape.reduce(1) do |product, size|
|
|
106
|
+
if !product.zero? && size > SIZE_MAX / product
|
|
107
|
+
raise RangeError, "#{description} exceeds native implementation limit"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
product * size
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def linear_result_shape(kernel_shape)
|
|
115
|
+
shape = extended_shape.zip(kernel_shape).map do |signal_size, kernel_size|
|
|
116
|
+
checked_add(signal_size, kernel_size - 1, 'FFT shape')
|
|
117
|
+
end
|
|
118
|
+
checked_shape(shape, 'FFT shape')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def checked_add(left, right, description)
|
|
122
|
+
return left + right if right <= SIZE_MAX - left
|
|
123
|
+
|
|
124
|
+
raise RangeError, "#{description} exceeds native implementation limit"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private_constant :OperationShapes
|
|
129
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'convolver/fft_limits'
|
|
4
|
+
|
|
5
|
+
module Convolver
|
|
6
|
+
# Selects a safe real-transform shape from exact/even and fast candidates.
|
|
7
|
+
# @private
|
|
8
|
+
class RealFftShape
|
|
9
|
+
def initialize(minimum_shape, size_max:, limits: nil)
|
|
10
|
+
@minimum_shape = minimum_shape
|
|
11
|
+
@size_max = size_max
|
|
12
|
+
@limits = limits
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
exact_shape = even_final_axis(minimum_shape)
|
|
17
|
+
candidates = [exact_shape, fast_shape(exact_shape)].compact.select { |shape| admissible?(shape) }
|
|
18
|
+
(candidates.min_by { |shape| transform_cost(shape) } || raise_rejection).freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :minimum_shape, :size_max, :limits
|
|
24
|
+
|
|
25
|
+
def admissible?(shape)
|
|
26
|
+
return false unless representable?(shape)
|
|
27
|
+
|
|
28
|
+
limits&.validate!(shape, real_axis: shape.length - 1)
|
|
29
|
+
true
|
|
30
|
+
rescue FftUnavailable => e
|
|
31
|
+
@rejection ||= e
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def raise_rejection
|
|
36
|
+
raise @rejection if @rejection
|
|
37
|
+
|
|
38
|
+
raise_overflow
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fast_shape(exact_shape)
|
|
42
|
+
fast_shape = exact_shape.map.with_index do |size, axis|
|
|
43
|
+
next_fast_size(size, even: axis == exact_shape.length - 1)
|
|
44
|
+
end
|
|
45
|
+
fast_shape if fast_shape.all?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def even_final_axis(shape)
|
|
49
|
+
result = shape.dup
|
|
50
|
+
result[-1] = checked_add(result[-1], 1) if result[-1].odd?
|
|
51
|
+
result.freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def next_fast_size(target, even:)
|
|
55
|
+
best = nil
|
|
56
|
+
each_power(5) do |power_of_five|
|
|
57
|
+
each_power(3, power_of_five) do |power_of_three|
|
|
58
|
+
candidate = doubled_candidate(power_of_three, target, even)
|
|
59
|
+
best = candidate if candidate && (!best || candidate < best)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
best
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def each_power(factor, initial = 1)
|
|
66
|
+
value = initial
|
|
67
|
+
loop do
|
|
68
|
+
yield value
|
|
69
|
+
break if value > size_max / factor
|
|
70
|
+
|
|
71
|
+
value *= factor
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def doubled_candidate(initial, target, even)
|
|
76
|
+
candidate = initial
|
|
77
|
+
while candidate < target || (even && candidate.odd?)
|
|
78
|
+
return nil if candidate > size_max / 2
|
|
79
|
+
|
|
80
|
+
candidate *= 2
|
|
81
|
+
end
|
|
82
|
+
candidate
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def transform_cost(shape)
|
|
86
|
+
shape.reduce(:*) * shape.sum { |size| Math.log2(size) + large_factor_penalty(size) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def large_factor_penalty(size)
|
|
90
|
+
prime_factors(size).sum do |factor|
|
|
91
|
+
factor > 7 ? 1.5 * (Math.log2(factor) - 3) : 0.0
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def prime_factors(value)
|
|
96
|
+
factors = []
|
|
97
|
+
divisor = 2
|
|
98
|
+
while divisor * divisor <= value
|
|
99
|
+
value = extract_factor(value, divisor, factors)
|
|
100
|
+
divisor += divisor == 2 ? 1 : 2
|
|
101
|
+
end
|
|
102
|
+
factors << value if value > 1
|
|
103
|
+
factors
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_factor(value, divisor, factors)
|
|
107
|
+
while (value % divisor).zero?
|
|
108
|
+
factors << divisor
|
|
109
|
+
value /= divisor
|
|
110
|
+
end
|
|
111
|
+
value
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def representable?(shape)
|
|
115
|
+
shape.reduce(1) do |product, size|
|
|
116
|
+
return false if !product.zero? && size > size_max / product
|
|
117
|
+
|
|
118
|
+
product * size
|
|
119
|
+
end
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def checked_add(left, right)
|
|
124
|
+
return left + right if right <= size_max - left
|
|
125
|
+
|
|
126
|
+
raise_overflow
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def raise_overflow
|
|
130
|
+
raise(limits ? FftUnavailable : RangeError, 'FFT shape exceeds native implementation limit')
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private_constant :RealFftShape
|
|
135
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Extends a signal according to one operation's boundary policy.
|
|
5
|
+
# @private
|
|
6
|
+
class SignalExtension
|
|
7
|
+
def initialize(shapes, boundary:, fill_value:)
|
|
8
|
+
@shapes = shapes
|
|
9
|
+
@boundary = boundary
|
|
10
|
+
@fill_value = fill_value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(signal)
|
|
14
|
+
source = Numo::SFloat.cast(signal)
|
|
15
|
+
return constant_extension(source) if boundary == :constant
|
|
16
|
+
|
|
17
|
+
source[*extension_indices]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :shapes, :boundary, :fill_value
|
|
23
|
+
|
|
24
|
+
def constant_extension(source)
|
|
25
|
+
extended = Numo::SFloat.new(*shapes.extended_shape).fill(fill_value)
|
|
26
|
+
ranges = source.shape.zip(shapes.padding_before).map do |signal_size, before|
|
|
27
|
+
before...(before + signal_size)
|
|
28
|
+
end
|
|
29
|
+
extended[*ranges] = source
|
|
30
|
+
extended
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def extension_indices
|
|
34
|
+
shapes.extended_shape.zip(shapes.padding_before).map.with_index do |(length, before), axis|
|
|
35
|
+
Array.new(length) { |offset| boundary_index(offset - before, signal_size(axis, before)) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def signal_size(axis, before)
|
|
40
|
+
shapes.extended_shape[axis] - before - shapes.padding_after[axis]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def boundary_index(position, size)
|
|
44
|
+
case boundary
|
|
45
|
+
when :nearest then position.clamp(0, size - 1)
|
|
46
|
+
when :wrap then position % size
|
|
47
|
+
when :reflect then reflect_index(position, size)
|
|
48
|
+
when :mirror then mirror_index(position, size)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def reflect_index(position, size)
|
|
53
|
+
residue = position % (2 * size)
|
|
54
|
+
residue < size ? residue : (2 * size) - 1 - residue
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def mirror_index(position, size)
|
|
58
|
+
return 0 if size == 1
|
|
59
|
+
|
|
60
|
+
period = 2 * (size - 1)
|
|
61
|
+
residue = position % period
|
|
62
|
+
residue < size ? residue : period - residue
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private_constant :SignalExtension
|
|
67
|
+
end
|
data/lib/convolver/version.rb
CHANGED