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.
@@ -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
- 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
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 dimensions
36
- return linear_dimensions unless plan.wrap?
37
-
38
- circular_dimensions
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 circular_dimensions
42
- real_axis = buffers.real_axis
43
- [buffers.transform_size, buffers.spectrum_size, transform_coefficient(real_axis), moved_size(real_axis)]
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 linear_dimensions
47
- [buffers.transform_size, buffers.spectrum_size,
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 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
48
+ def preparation_cost
49
+ return circular_preparation_cost if plan.wrap?
56
50
 
57
- def cost_for(costs)
58
- costs.fetch(signal.ndim, costs.values.last)
51
+ (2 * FftCost::PREPARATION * buffers.transform_size) + preparation.extension
59
52
  end
60
53
 
61
- def transform_coefficient(real_axis)
62
- cost_for(real_axis ? REAL_FFT_COST : COMPLEX_FFT_COST)
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(real_axis)
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
- Numo::SFloat.cast(full_result[*valid_ranges])
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 => 6.8e-10, 2 => 5.9e-10, 3 => 4.5e-10 }.freeze
8
- EXTENSION_COST = 1.0e-9
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
- 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:)
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(receiver)
37
- return invoke(receiver, :basic) if plan.extended_size < 1000
24
+ def automatic
25
+ return basic if plan.result_shape.empty?
38
26
 
39
- direct_time = invoke(receiver, :predict_basic)
40
- return invoke(receiver, :fft) if automatic_fft_time(receiver) < FFT_SPEEDUP_MARGIN * direct_time
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
- invoke(receiver, :basic)
31
+ basic
43
32
  end
44
33
 
45
34
  def basic
46
35
  plan.validate_basic!
47
- extended_signal = plan.extend_signal(signal)
48
- return Convolver.send(:correlate_basic_valid, extended_signal, kernel) if operation == :correlation
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, kernel)
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 Numo::SFloat.cast(signal) * Numo::SFloat.cast(kernel) if signal.ndim.zero?
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 = 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
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 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
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
- return CircularFftOperation.new(operation, signal, kernel, plan) if plan.wrap?
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(signal), kernel, plan)
75
+ LinearFftOperation.new(operation, plan.extend_signal(prepared_signal), prepared_kernel, plan)
93
76
  end
94
77
 
95
- def automatic_fft_time(receiver)
96
- invoke(receiver, :predict_fft)
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
- DIRECT_OPERATION_COST.fetch(signal.ndim, DIRECT_OPERATION_COST.values.last)
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
- def initialize(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
27
- @options = OperationOptions.new(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
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
- 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?
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
- 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
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 = Numo::SFloat.cast(signal)
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 = Numo::SFloat.new(*shapes.extended_shape).fill(fill_value)
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Convolver
4
4
  # Current gem version.
5
- VERSION = '3.0.1'
5
+ VERSION = '4.0.0'
6
6
  end