convolver 3.0.1 → 4.0.0
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 +31 -2
- data/README.md +55 -4
- data/ext/convolver/convolve_raw.c +51 -165
- data/ext/convolver/convolve_raw.h +5 -19
- data/ext/convolver/convolver.c +42 -20
- data/ext/convolver/correlate_raw.c +51 -161
- data/ext/convolver/correlate_raw.h +5 -19
- data/ext/convolver/extconf.rb +1 -0
- data/ext/convolver/fft_shape.c +116 -0
- data/ext/convolver/fft_shape.h +8 -0
- data/ext/convolver/raw_config.h +11 -0
- data/ext/convolver/raw_helpers.h +71 -0
- data/ext/convolver/raw_loop.h +93 -0
- data/lib/convolver/buffer_preparation.rb +4 -4
- data/lib/convolver/circular_fft_operation.rb +4 -3
- data/lib/convolver/cost_profiles.rb +44 -0
- data/lib/convolver/fft_cost.rb +52 -0
- data/lib/convolver/fft_estimator.rb +25 -32
- data/lib/convolver/linear_fft_operation.rb +4 -2
- data/lib/convolver/operation_dtype.rb +42 -0
- data/lib/convolver/operation_execution.rb +56 -45
- data/lib/convolver/operation_options.rb +4 -5
- data/lib/convolver/operation_plan.rb +16 -3
- data/lib/convolver/preparation_cost.rb +46 -0
- data/lib/convolver/real_fft_shape.rb +2 -61
- data/lib/convolver/signal_extension.rb +2 -2
- data/lib/convolver/version.rb +1 -1
- data/lib/convolver.rb +35 -31
- metadata +10 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'convolver/preparation_cost'
|
|
4
|
+
|
|
5
|
+
module Convolver
|
|
6
|
+
# Shared transform costs and a lower bound that never constructs an FFT plan.
|
|
7
|
+
# @private
|
|
8
|
+
class FftCost
|
|
9
|
+
REAL = { 1 => 1.0e-9, 2 => 1.0e-9, 3 => 1.5e-9 }.freeze
|
|
10
|
+
COMPLEX = { 1 => 2.4e-9, 2 => 2.4e-9, 3 => 2.8e-9 }.freeze
|
|
11
|
+
FIXED = { 0 => 5.0e-6, 1 => 1.5e-5, 2 => 3.0e-5, 3 => 1.0e-4 }.freeze
|
|
12
|
+
PREPARATION = 1.0e-9
|
|
13
|
+
|
|
14
|
+
def self.fixed(rank, profile: nil)
|
|
15
|
+
return profile.fetch(:fixed).fetch(rank - 1) if profile
|
|
16
|
+
|
|
17
|
+
FIXED.fetch(rank, FIXED.values.last)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.transform(size, rank, real: true, profile: nil)
|
|
21
|
+
coefficient(rank, real:, profile:) * size * Math.log([size, 2].max)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.lower_bound(plan, kernel_shape, threshold:)
|
|
25
|
+
rank = kernel_shape.length
|
|
26
|
+
profile = plan.cost_profile
|
|
27
|
+
setup = fixed(rank, profile:)
|
|
28
|
+
return setup if setup >= threshold || plan.wrap? || rank.zero?
|
|
29
|
+
|
|
30
|
+
size = minimum_size(plan.extended_shape, kernel_shape)
|
|
31
|
+
setup + transform(size, rank, profile:) + (2 * PREPARATION * size) + PreparationCost.new(plan).indices
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.coefficient(rank, real:, profile:)
|
|
35
|
+
return profile.fetch(:transform).fetch(rank - 1) * (real ? 1 : 2.2) if profile
|
|
36
|
+
|
|
37
|
+
costs = real ? REAL : COMPLEX
|
|
38
|
+
costs.fetch(rank, costs.values.last)
|
|
39
|
+
end
|
|
40
|
+
private_class_method :coefficient
|
|
41
|
+
|
|
42
|
+
def self.minimum_size(signal_shape, kernel_shape)
|
|
43
|
+
# Ruby integers avoid overflow: this is only a cost bound, never an allocation.
|
|
44
|
+
shape = signal_shape.zip(kernel_shape).map { |signal, kernel| signal + kernel - 1 }
|
|
45
|
+
shape[-1] += 1 if shape[-1].odd?
|
|
46
|
+
shape.reduce(1, :*)
|
|
47
|
+
end
|
|
48
|
+
private_class_method :minimum_size
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private_constant :FftCost
|
|
52
|
+
end
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'convolver/fft_cost'
|
|
4
|
+
|
|
3
5
|
module Convolver
|
|
4
6
|
# Estimates the selected real or complex PocketFFT implementation cost.
|
|
5
7
|
# @private
|
|
6
8
|
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
9
|
AXIS_MOVE_COST = 1.6e-9
|
|
10
|
+
RESULT_COST = 5.0e-10
|
|
12
11
|
CORRELATION_COST = 2.0e-10
|
|
13
12
|
|
|
14
13
|
def initialize(operation, signal, kernel, plan)
|
|
@@ -16,54 +15,48 @@ module Convolver
|
|
|
16
15
|
@signal = signal
|
|
17
16
|
@kernel = kernel
|
|
18
17
|
@plan = plan
|
|
18
|
+
@profile = plan.cost_profile
|
|
19
19
|
@buffers = plan.fft_buffers
|
|
20
|
+
@preparation = PreparationCost.new(plan)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def call
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
24
|
+
return FftCost.fixed(0) + preparation.conversion + result_cost if signal.ndim.zero?
|
|
25
|
+
|
|
26
|
+
transform_cost + preparation_cost + extra_cost
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
private
|
|
32
30
|
|
|
33
|
-
attr_reader :operation, :signal, :kernel, :plan, :buffers
|
|
31
|
+
attr_reader :operation, :signal, :kernel, :plan, :buffers, :preparation
|
|
34
32
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
def transform_cost
|
|
34
|
+
transform = FftCost.transform(buffers.transform_size, signal.ndim,
|
|
35
|
+
real: !buffers.real_axis.nil?, profile: @profile)
|
|
36
|
+
FftCost.fixed(signal.ndim, profile: @profile) + transform + (AXIS_MOVE_COST * moved_size)
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
def extra_cost
|
|
40
|
+
correlation_cost = operation == :correlation ? CORRELATION_COST * buffers.spectrum_size : 0.0
|
|
41
|
+
correlation_cost + preparation.conversion + result_cost
|
|
44
42
|
end
|
|
45
43
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
cost_for(REAL_FFT_COST), 0]
|
|
44
|
+
def result_cost
|
|
45
|
+
RESULT_COST * plan.result_size * (plan.dtype::ELEMENT_BYTE_SIZE / 4)
|
|
49
46
|
end
|
|
50
47
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
working_size = plan.wrap? ? signal.size + kernel.size : 2 * transform_size
|
|
54
|
-
extension_size + working_size
|
|
55
|
-
end
|
|
48
|
+
def preparation_cost
|
|
49
|
+
return circular_preparation_cost if plan.wrap?
|
|
56
50
|
|
|
57
|
-
|
|
58
|
-
costs.fetch(signal.ndim, costs.values.last)
|
|
51
|
+
(2 * FftCost::PREPARATION * buffers.transform_size) + preparation.extension
|
|
59
52
|
end
|
|
60
53
|
|
|
61
|
-
def
|
|
62
|
-
|
|
54
|
+
def circular_preparation_cost
|
|
55
|
+
(FftCost::PREPARATION * (signal.size + kernel.size)) + preparation.folding(kernel.size)
|
|
63
56
|
end
|
|
64
57
|
|
|
65
|
-
def moved_size
|
|
66
|
-
real_axis && real_axis != signal.ndim - 1 ? 3 * signal.size : 0
|
|
58
|
+
def moved_size
|
|
59
|
+
buffers.real_axis && buffers.real_axis != signal.ndim - 1 ? 3 * signal.size : 0
|
|
67
60
|
end
|
|
68
61
|
end
|
|
69
62
|
|
|
@@ -5,6 +5,7 @@ module Convolver
|
|
|
5
5
|
# @private
|
|
6
6
|
class LinearFftOperation
|
|
7
7
|
def initialize(operation, signal, kernel, plan)
|
|
8
|
+
@dtype = plan.dtype
|
|
8
9
|
@operation = operation
|
|
9
10
|
@signal = signal
|
|
10
11
|
@kernel = kernel
|
|
@@ -15,12 +16,13 @@ module Convolver
|
|
|
15
16
|
signal_padded = zero_padded(signal)
|
|
16
17
|
kernel_padded = zero_padded(kernel)
|
|
17
18
|
full_result = real_spectrum_product(signal_padded, kernel_padded)
|
|
18
|
-
|
|
19
|
+
cropped = full_result[*valid_ranges]
|
|
20
|
+
dtype == Numo::DFloat ? cropped.dup : dtype.cast(cropped)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
private
|
|
22
24
|
|
|
23
|
-
attr_reader :operation, :signal, :kernel, :transform_shape
|
|
25
|
+
attr_reader :operation, :signal, :kernel, :transform_shape, :dtype
|
|
24
26
|
|
|
25
27
|
def zero_padded(value)
|
|
26
28
|
Numo::DFloat.zeros(*transform_shape).tap do |padded|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Resolves public precision without allocating operand copies.
|
|
5
|
+
# @private
|
|
6
|
+
class OperationDtype
|
|
7
|
+
SINGLE_INPUTS = [Numo::SFloat, Numo::Int8, Numo::UInt8, Numo::Int16, Numo::UInt16].freeze
|
|
8
|
+
DOUBLE_INPUTS = [Numo::DFloat, Numo::Int32, Numo::UInt32, Numo::Int64, Numo::UInt64].freeze
|
|
9
|
+
INPUTS = (SINGLE_INPUTS + DOUBLE_INPUTS).freeze
|
|
10
|
+
OUTPUTS = [Numo::SFloat, Numo::DFloat].freeze
|
|
11
|
+
|
|
12
|
+
def self.resolve(signal, kernel, dtype)
|
|
13
|
+
validate_inputs!(signal, kernel)
|
|
14
|
+
validate_dtype!(dtype)
|
|
15
|
+
types = [signal.class, kernel.class]
|
|
16
|
+
dtype || (types.any? { |type| DOUBLE_INPUTS.include?(type) } ? Numo::DFloat : Numo::SFloat)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.validate_inputs!(signal, kernel)
|
|
20
|
+
unless signal.is_a?(Numo::NArray) && kernel.is_a?(Numo::NArray)
|
|
21
|
+
raise ArgumentError, 'signal and kernel must be Numo::NArray values'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
return if [signal.class, kernel.class].all? { |type| INPUTS.include?(type) }
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, 'unsupported signal or kernel dtype'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.validate_dtype!(dtype)
|
|
30
|
+
return if dtype.nil? || OUTPUTS.include?(dtype)
|
|
31
|
+
|
|
32
|
+
raise ArgumentError, 'dtype must be Numo::SFloat, Numo::DFloat, or nil'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.cast_fill(value, dtype)
|
|
36
|
+
# A scalar IEEE float conversion, without allocating a Numo buffer before preflight.
|
|
37
|
+
dtype == Numo::SFloat ? [value].pack('f').unpack1('f') : value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private_constant :OperationDtype
|
|
42
|
+
end
|
|
@@ -4,26 +4,13 @@ module Convolver
|
|
|
4
4
|
# Coordinates validation, implementation selection, and one operation family.
|
|
5
5
|
# @private
|
|
6
6
|
class OperationExecution
|
|
7
|
-
DIRECT_OPERATION_COST = { 1 =>
|
|
8
|
-
|
|
7
|
+
DIRECT_OPERATION_COST = { 1 => 3.8e-10, 2 => 5.9e-10, 3 => 4.5e-10 }.freeze
|
|
8
|
+
DOUBLE_OPERATION_COST = { 1 => 3.9e-10, 2 => 6.0e-10, 3 => 4.6e-10 }.freeze
|
|
9
|
+
LARGE_KERNEL_COST = 6.5e-10
|
|
9
10
|
FFT_SPEEDUP_MARGIN = 0.8
|
|
11
|
+
CALIBRATED_FFT_SPEEDUP_MARGIN = 0.9
|
|
10
12
|
|
|
11
|
-
|
|
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:)
|
|
13
|
+
def initialize(operation, signal, kernel, mode:, boundary:, fill_value:, origin:, dtype:)
|
|
27
14
|
@operation = operation
|
|
28
15
|
@signal = signal
|
|
29
16
|
@kernel = kernel
|
|
@@ -31,28 +18,31 @@ module Convolver
|
|
|
31
18
|
@boundary = boundary
|
|
32
19
|
@fill_value = fill_value
|
|
33
20
|
@origin = origin
|
|
21
|
+
@dtype = dtype
|
|
34
22
|
end
|
|
35
23
|
|
|
36
|
-
def automatic
|
|
37
|
-
return
|
|
24
|
+
def automatic
|
|
25
|
+
return basic if plan.result_shape.empty?
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
return
|
|
27
|
+
threshold = fft_margin * basic_time
|
|
28
|
+
return basic if FftCost.lower_bound(plan, kernel.shape, threshold:) >= threshold
|
|
29
|
+
return fft if automatic_fft_time < threshold
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
basic
|
|
43
32
|
end
|
|
44
33
|
|
|
45
34
|
def basic
|
|
46
35
|
plan.validate_basic!
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
prepared_signal, prepared_kernel = prepared_inputs
|
|
37
|
+
extended_signal = plan.extend_signal(prepared_signal)
|
|
38
|
+
return Convolver.send(:correlate_basic_valid, extended_signal, prepared_kernel) if operation == :correlation
|
|
49
39
|
|
|
50
|
-
Convolver.send(:convolve_basic_valid, extended_signal,
|
|
40
|
+
Convolver.send(:convolve_basic_valid, extended_signal, prepared_kernel)
|
|
51
41
|
end
|
|
52
42
|
|
|
53
43
|
def fft
|
|
54
44
|
plan.fft_buffers
|
|
55
|
-
return
|
|
45
|
+
return prepared_inputs.reduce(:*) if signal.ndim.zero?
|
|
56
46
|
|
|
57
47
|
fft_operation.call
|
|
58
48
|
end
|
|
@@ -63,43 +53,64 @@ module Convolver
|
|
|
63
53
|
|
|
64
54
|
def basic_time
|
|
65
55
|
plan.validate_basic!
|
|
66
|
-
calculation_cost
|
|
67
|
-
extension_cost = plan.valid? ? 0.0 : EXTENSION_COST * plan.extended_size
|
|
68
|
-
calculation_cost + extension_cost
|
|
56
|
+
calculation_cost + preparation_cost
|
|
69
57
|
end
|
|
70
58
|
|
|
71
59
|
private
|
|
72
60
|
|
|
73
|
-
attr_reader :operation, :signal, :kernel, :mode, :boundary, :fill_value, :origin
|
|
61
|
+
attr_reader :operation, :signal, :kernel, :mode, :boundary, :fill_value, :origin, :dtype
|
|
74
62
|
|
|
75
63
|
def plan
|
|
76
|
-
@plan ||= OperationPlan.new(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
|
|
64
|
+
@plan ||= OperationPlan.new(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:, dtype:)
|
|
77
65
|
end
|
|
78
66
|
|
|
79
|
-
def
|
|
80
|
-
|
|
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
|
|
67
|
+
def prepared_inputs
|
|
68
|
+
@prepared_inputs ||= [plan.dtype.cast(signal), plan.dtype.cast(kernel)]
|
|
87
69
|
end
|
|
88
70
|
|
|
89
71
|
def fft_operation
|
|
90
|
-
|
|
72
|
+
prepared_signal, prepared_kernel = prepared_inputs
|
|
73
|
+
return CircularFftOperation.new(operation, prepared_signal, prepared_kernel, plan) if plan.wrap?
|
|
91
74
|
|
|
92
|
-
LinearFftOperation.new(operation, plan.extend_signal(
|
|
75
|
+
LinearFftOperation.new(operation, plan.extend_signal(prepared_signal), prepared_kernel, plan)
|
|
93
76
|
end
|
|
94
77
|
|
|
95
|
-
def automatic_fft_time
|
|
96
|
-
|
|
78
|
+
def automatic_fft_time
|
|
79
|
+
fft_time
|
|
97
80
|
rescue FftUnavailable
|
|
98
81
|
Float::INFINITY
|
|
99
82
|
end
|
|
100
83
|
|
|
84
|
+
def preparation_cost
|
|
85
|
+
costs = PreparationCost.new(plan)
|
|
86
|
+
costs.conversion + costs.extension
|
|
87
|
+
end
|
|
88
|
+
|
|
101
89
|
def direct_operation_cost
|
|
102
|
-
|
|
90
|
+
return LARGE_KERNEL_COST if kernel.size > 64 && signal.ndim != 2
|
|
91
|
+
|
|
92
|
+
costs = plan.dtype == Numo::DFloat ? DOUBLE_OPERATION_COST : DIRECT_OPERATION_COST
|
|
93
|
+
costs.fetch(signal.ndim, costs.values.last)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def calculation_cost
|
|
97
|
+
return calibrated_calculation_cost if plan.cost_profile
|
|
98
|
+
|
|
99
|
+
direct_operation_cost * plan.result_size * kernel.size
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def calibrated_calculation_cost
|
|
103
|
+
fixed, short, long = plan.cost_profile.fetch(:direct).fetch(plan.dtype == Numo::DFloat ? 1 : 0)
|
|
104
|
+
fixed + (kernel_work(short, long) * plan.result_size)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def kernel_work(short, long)
|
|
108
|
+
size = kernel.size
|
|
109
|
+
(short * [size, 64].min) + (long * [size - 64, 0].max)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fft_margin
|
|
113
|
+
plan.cost_profile ? CALIBRATED_FFT_SPEEDUP_MARGIN : FFT_SPEEDUP_MARGIN
|
|
103
114
|
end
|
|
104
115
|
end
|
|
105
116
|
|
|
@@ -8,21 +8,20 @@ module Convolver
|
|
|
8
8
|
MODES = %i[valid same full].freeze
|
|
9
9
|
BOUNDARIES = %i[constant nearest reflect mirror wrap].freeze
|
|
10
10
|
|
|
11
|
-
attr_reader :operation, :mode, :boundary, :fill_value, :origins, :anchors
|
|
11
|
+
attr_reader :operation, :mode, :boundary, :fill_value, :origins, :anchors, :dtype
|
|
12
12
|
|
|
13
|
-
def initialize(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
|
|
13
|
+
def initialize(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:, dtype:)
|
|
14
|
+
@dtype = OperationDtype.resolve(signal, kernel, dtype)
|
|
14
15
|
validate_inputs!(signal, kernel)
|
|
15
16
|
validate_vocabulary!(operation, mode, boundary)
|
|
16
17
|
assign_options(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
|
|
17
18
|
validate_combinations!(signal.shape, kernel.shape)
|
|
19
|
+
@fill_value = OperationDtype.cast_fill(@fill_value, @dtype)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
private
|
|
21
23
|
|
|
22
24
|
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
25
|
raise ArgumentError, 'signal and kernel must not be empty' if signal.empty? || kernel.empty?
|
|
27
26
|
raise ArgumentError, 'signal and kernel must have equal rank' unless signal.ndim == kernel.ndim
|
|
28
27
|
raise ArgumentError, "maximum supported rank is #{MAX_RANK}" if signal.ndim > MAX_RANK
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'forwardable'
|
|
4
|
+
require 'convolver/cost_profiles'
|
|
5
|
+
require 'convolver/operation_dtype'
|
|
4
6
|
require 'convolver/operation_options'
|
|
5
7
|
require 'convolver/operation_shapes'
|
|
6
8
|
require 'convolver/signal_extension'
|
|
@@ -18,15 +20,19 @@ module Convolver
|
|
|
18
20
|
class OperationPlan
|
|
19
21
|
extend Forwardable
|
|
20
22
|
|
|
21
|
-
def_delegators :options, :operation, :mode, :boundary, :fill_value, :origins, :anchors
|
|
23
|
+
def_delegators :options, :operation, :mode, :boundary, :fill_value, :origins, :anchors, :dtype
|
|
22
24
|
def_delegators :shapes, :padding_before, :padding_after, :result_shape,
|
|
23
25
|
:extended_shape, :result_size, :extended_size, :linear_fft_shape,
|
|
24
26
|
:linear_fft_size, :linear_spectrum_size
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
attr_reader :conversion_size, :cost_profile
|
|
29
|
+
|
|
30
|
+
def initialize(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:, dtype: nil)
|
|
31
|
+
@options = OperationOptions.new(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:, dtype:)
|
|
32
|
+
@conversion_size = input_conversion_size(signal, kernel)
|
|
28
33
|
@signal_shape = signal.shape.freeze
|
|
29
34
|
@kernel_shape = kernel.shape.freeze
|
|
35
|
+
@cost_profile = CostProfiles.for_rank(@signal_shape.length)
|
|
30
36
|
@shapes = OperationShapes.new(
|
|
31
37
|
signal.shape, kernel.shape, operation:, mode:, anchors: options.anchors
|
|
32
38
|
)
|
|
@@ -47,7 +53,10 @@ module Convolver
|
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
def validate_basic!
|
|
56
|
+
return if @basic_validated
|
|
57
|
+
|
|
50
58
|
BufferPreparation.new(self, @signal_shape, @kernel_shape).basic!
|
|
59
|
+
@basic_validated = true
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
def fft_buffers
|
|
@@ -57,6 +66,10 @@ module Convolver
|
|
|
57
66
|
private
|
|
58
67
|
|
|
59
68
|
attr_reader :options, :shapes
|
|
69
|
+
|
|
70
|
+
def input_conversion_size(signal, kernel)
|
|
71
|
+
[signal, kernel].sum { |input| input.instance_of?(dtype) ? 0 : input.size }
|
|
72
|
+
end
|
|
60
73
|
end
|
|
61
74
|
|
|
62
75
|
private_constant :OperationPlan, :UNSPECIFIED_FILL
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Shared estimates for input copies and Ruby boundary/folding work.
|
|
5
|
+
# @private
|
|
6
|
+
class PreparationCost
|
|
7
|
+
CONVERSION_COST = 7.5e-10
|
|
8
|
+
EXTENSION_COST = 1.0e-9
|
|
9
|
+
INDEX_COST = { nearest: 1.45e-7, reflect: 1.33e-7, mirror: 1.54e-7, wrap: 1.05e-7 }.freeze
|
|
10
|
+
FOLD_COST = { 1 => 3.3e-7, 2 => 3.7e-7, 3 => 5.5e-7 }.freeze
|
|
11
|
+
|
|
12
|
+
def initialize(plan)
|
|
13
|
+
@plan = plan
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def conversion
|
|
17
|
+
CONVERSION_COST * plan.conversion_size
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def extension
|
|
21
|
+
return 0.0 if plan.valid?
|
|
22
|
+
|
|
23
|
+
(EXTENSION_COST * plan.extended_size * (plan.dtype::ELEMENT_BYTE_SIZE / 4)) + indices
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def indices
|
|
27
|
+
return 0.0 if plan.valid? || plan.boundary == :constant
|
|
28
|
+
|
|
29
|
+
costs = plan.cost_profile ? plan.cost_profile.fetch(:indices) : INDEX_COST
|
|
30
|
+
costs.fetch(plan.boundary) * plan.extended_shape.sum
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def folding(kernel_size)
|
|
34
|
+
rank = plan.result_shape.length
|
|
35
|
+
return plan.cost_profile.fetch(:folding).fetch(rank - 1) * kernel_size if plan.cost_profile
|
|
36
|
+
|
|
37
|
+
FOLD_COST.fetch(rank, FOLD_COST.values.last) * kernel_size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :plan
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private_constant :PreparationCost
|
|
46
|
+
end
|
|
@@ -39,10 +39,7 @@ module Convolver
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def fast_shape(exact_shape)
|
|
42
|
-
|
|
43
|
-
next_fast_size(size, even: axis == exact_shape.length - 1)
|
|
44
|
-
end
|
|
45
|
-
fast_shape if fast_shape.all?
|
|
42
|
+
Convolver.send(:fft_fast_shape, exact_shape, size_max)
|
|
46
43
|
end
|
|
47
44
|
|
|
48
45
|
def even_final_axis(shape)
|
|
@@ -51,64 +48,8 @@ module Convolver
|
|
|
51
48
|
result.freeze
|
|
52
49
|
end
|
|
53
50
|
|
|
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
51
|
def transform_cost(shape)
|
|
86
|
-
|
|
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
|
|
52
|
+
Convolver.send(:fft_shape_cost, shape)
|
|
112
53
|
end
|
|
113
54
|
|
|
114
55
|
def representable?(shape)
|
|
@@ -11,7 +11,7 @@ module Convolver
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def call(signal)
|
|
14
|
-
source =
|
|
14
|
+
source = signal
|
|
15
15
|
return constant_extension(source) if boundary == :constant
|
|
16
16
|
|
|
17
17
|
source[*extension_indices]
|
|
@@ -22,7 +22,7 @@ module Convolver
|
|
|
22
22
|
attr_reader :shapes, :boundary, :fill_value
|
|
23
23
|
|
|
24
24
|
def constant_extension(source)
|
|
25
|
-
extended =
|
|
25
|
+
extended = source.class.new(*shapes.extended_shape).fill(fill_value)
|
|
26
26
|
ranges = source.shape.zip(shapes.padding_before).map do |signal_size, before|
|
|
27
27
|
before...(before + signal_size)
|
|
28
28
|
end
|
data/lib/convolver/version.rb
CHANGED