convolver 3.0.1 → 4.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.
@@ -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,12 +1,14 @@
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'
7
9
  require 'convolver/fft_buffer_plan'
8
10
 
9
- # Internal planning and extension support for Convolver's public operations.
11
+ # @!macro convolver_overview
10
12
  module Convolver
11
13
  # Distinguishes an omitted fill_value keyword from an explicitly supplied
12
14
  # value. This lets non-constant boundaries reject even an explicit zero.
@@ -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.1'
6
6
  end