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.
@@ -1,33 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'convolver/real_fft_shape'
4
+ require 'convolver/fft_limits'
5
+
3
6
  module Convolver
4
7
  # Calculates and validates output, extension, and FFT dimensions.
8
+ # @private
5
9
  class OperationShapes
6
10
  SIZE_MAX = (1 << ([0].pack('J').bytesize * 8)) - 1
7
11
 
8
12
  attr_reader :padding_before, :padding_after, :result_shape,
9
13
  :extended_shape, :result_size, :extended_size
10
14
 
11
- def initialize(signal_shape, kernel_shape, mode:, anchors:)
15
+ def initialize(signal_shape, kernel_shape, operation:, mode:, anchors:)
16
+ @operation = operation
12
17
  @mode = mode
13
18
  calculate_shapes(signal_shape, kernel_shape, anchors)
14
19
  validate_sizes!
15
20
  end
16
21
 
17
- def linear_fft_shape(kernel_shape)
18
- checked_shape(
19
- extended_shape.zip(kernel_shape).map { |signal_size, kernel_size| signal_size + kernel_size - 1 },
20
- 'FFT shape'
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
22
26
  end
23
27
 
24
28
  def linear_fft_size(kernel_shape)
25
29
  checked_product(linear_fft_shape(kernel_shape), 'FFT size')
26
30
  end
27
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
+
28
41
  private
29
42
 
30
- attr_reader :mode
43
+ attr_reader :operation, :mode
31
44
 
32
45
  def calculate_shapes(signal_shape, kernel_shape, anchors)
33
46
  @padding_before, @padding_after, @result_shape = shapes_for(signal_shape, kernel_shape, anchors)
@@ -53,8 +66,15 @@ module Convolver
53
66
  end
54
67
 
55
68
  def same_shapes(signal_shape, kernel_shape, anchors)
56
- before = anchors.dup.freeze
57
- after = kernel_shape.zip(anchors).map { |kernel_size, anchor| kernel_size - 1 - anchor }.freeze
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
58
78
  [before, after, signal_shape.dup]
59
79
  end
60
80
 
@@ -90,6 +110,19 @@ module Convolver
90
110
  product * size
91
111
  end
92
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
93
126
  end
94
127
 
95
128
  private_constant :OperationShapes
@@ -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
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Convolver
4
4
  # Extends a signal according to one operation's boundary policy.
5
+ # @private
5
6
  class SignalExtension
6
7
  def initialize(shapes, boundary:, fill_value:)
7
8
  @shapes = shapes
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Convolver
4
4
  # Current gem version.
5
- VERSION = '2.0.0'
5
+ VERSION = '3.0.1'
6
6
  end
data/lib/convolver.rb CHANGED
@@ -5,153 +5,136 @@ require 'numo/pocketfft'
5
5
  require 'convolver/convolver'
6
6
  require 'convolver/version'
7
7
 
8
- # Cross-correlation operations for Numo::NArray values.
8
+ # Mathematical convolution and cross-correlation for Numo::NArray values.
9
9
  module Convolver
10
10
  # Maximum number of dimensions supported by the implementations.
11
11
  MAX_RANK = 16
12
12
 
13
13
  require 'convolver/operation_plan'
14
+ require 'convolver/linear_fft_operation'
15
+ require 'convolver/circular_fft_operation'
16
+ require 'convolver/fft_estimator'
17
+ require 'convolver/operation_execution'
14
18
 
15
19
  class << self
16
- # Chooses the likely fastest cross-correlation implementation.
20
+ # Chooses the likely fastest mathematical convolution implementation.
17
21
  #
18
- # @param signal [Numo::NArray] input values
19
- # @param kernel [Numo::NArray] correlation kernel
20
- # @param mode [:valid, :same, :full] returned output extent
21
- # @param boundary [:constant, :nearest, :reflect, :mirror, :wrap] signal extension
22
- # @param fill_value [Numeric] constant extension value
23
- # @param origin [Integer, Array<Integer>] kernel origin shift
24
- # @return [Numo::SFloat] cross-correlation result
25
- # @raise [ArgumentError] if inputs or options are incompatible
22
+ # Results use single precision. Inputs must be nonempty arrays of equal
23
+ # rank, with at most {MAX_RANK} dimensions. Scalars multiply directly.
24
+ # In valid mode the kernel must fit inside the signal in every dimension.
25
+ # Full mode permits only constant boundaries; nonconstant boundaries and
26
+ # nonzero origins are supported only in same mode.
27
+ #
28
+ # @param signal [Numo::NArray] input signal
29
+ # @param kernel [Numo::NArray] convolution kernel
30
+ # @param mode [:valid, :same, :full] output extent (default: :valid)
31
+ # @param boundary [:constant, :nearest, :reflect, :mirror, :wrap]
32
+ # signal extension policy (default: :constant)
33
+ # @param fill_value [Numeric] constant boundary value (default: 0);
34
+ # valid mode requires zero, and other boundaries reject this option
35
+ # @param origin [Integer, Array<Integer>] offset from the kernel midpoint,
36
+ # shared across axes or one per axis (default: 0)
37
+ # @return [Numo::SFloat] mathematical convolution result
38
+ # @raise [ArgumentError] if inputs or options are invalid
39
+ # @raise [RangeError] if planned dimensions, buffers or FFT arithmetic exceed native limits
26
40
  def convolve(signal, kernel, mode: :valid, boundary: :constant,
27
41
  fill_value: UNSPECIFIED_FILL, origin: 0)
28
- plan = operation_plan(signal, kernel, mode:, boundary:, fill_value:, origin:)
29
- options = operation_options(mode, boundary, fill_value, origin)
30
-
31
- return convolve_basic(signal, kernel, **options) if plan.extended_size < 1000 || kernel.size < 100
32
-
33
- basic_time_predicted = predict_convolve_basic_time(signal, kernel, **options)
34
- return convolve_basic(signal, kernel, **options) if basic_time_predicted < 0.1
35
-
36
- fft_time_predicted = predict_convolve_fft_time(signal, kernel, **options)
37
- return convolve_fft(signal, kernel, **options) if fft_time_predicted < 2 * basic_time_predicted
38
-
39
- convolve_basic(signal, kernel, **options)
42
+ execution(:convolution, signal, kernel, mode:, boundary:, fill_value:, origin:).automatic(self)
40
43
  end
41
44
 
42
- # Uses the direct native valid primitive after applying the requested signal
43
- # extension in Ruby.
44
- #
45
+ # Chooses the likely fastest cross-correlation implementation.
46
+ # Uses the same input and option rules as {.convolve}, without reversing
47
+ # the kernel's orientation.
48
+ # @param (see convolve)
45
49
  # @return [Numo::SFloat] cross-correlation result
50
+ # @raise (see convolve)
51
+ def correlate(signal, kernel, mode: :valid, boundary: :constant,
52
+ fill_value: UNSPECIFIED_FILL, origin: 0)
53
+ execution(:correlation, signal, kernel, mode:, boundary:, fill_value:, origin:).automatic(self)
54
+ end
55
+
56
+ # Uses the direct native mathematical convolution implementation.
57
+ # @param (see convolve)
58
+ # @return [Numo::SFloat] mathematical convolution result
59
+ # @raise (see convolve)
46
60
  def convolve_basic(signal, kernel, mode: :valid, boundary: :constant,
47
61
  fill_value: UNSPECIFIED_FILL, origin: 0)
48
- plan = operation_plan(signal, kernel, mode:, boundary:, fill_value:, origin:)
49
- convolve_basic_valid(plan.extend_signal(signal), kernel)
62
+ execution(:convolution, signal, kernel, mode:, boundary:, fill_value:, origin:).basic
50
63
  end
51
64
 
52
- # Uses PocketFFT to calculate the requested cross-correlation.
53
- #
54
- # Periodic same-sized results use a circular transform. Other combinations
55
- # use the shared extension plan and PocketFFT's linear convolution.
56
- #
65
+ # Uses the direct native cross-correlation implementation.
66
+ # @param (see convolve)
57
67
  # @return [Numo::SFloat] cross-correlation result
68
+ # @raise (see convolve)
69
+ def correlate_basic(signal, kernel, mode: :valid, boundary: :constant,
70
+ fill_value: UNSPECIFIED_FILL, origin: 0)
71
+ execution(:correlation, signal, kernel, mode:, boundary:, fill_value:, origin:).basic
72
+ end
73
+
74
+ # Uses PocketFFT to calculate mathematical convolution.
75
+ # @param (see convolve)
76
+ # @return [Numo::SFloat] mathematical convolution result
77
+ # @raise (see convolve)
58
78
  def convolve_fft(signal, kernel, mode: :valid, boundary: :constant,
59
79
  fill_value: UNSPECIFIED_FILL, origin: 0)
60
- plan = operation_plan(signal, kernel, mode:, boundary:, fill_value:, origin:)
61
- return convolve_fft_wrap(signal, kernel, plan) if plan.wrap?
62
-
63
- convolve_fft_valid(plan.extend_signal(signal), kernel)
80
+ execution(:convolution, signal, kernel, mode:, boundary:, fill_value:, origin:).fft
64
81
  end
65
82
 
66
- # Compatibility alias for the former FFTW3-backed implementation.
67
- #
68
- # @deprecated Use {.convolve_fft}; Convolver no longer uses FFTW3.
83
+ # Uses PocketFFT to calculate cross-correlation.
84
+ # @param (see convolve)
69
85
  # @return [Numo::SFloat] cross-correlation result
70
- def convolve_fftw3(signal, kernel, mode: :valid, boundary: :constant,
71
- fill_value: UNSPECIFIED_FILL, origin: 0)
72
- warn 'Convolver.convolve_fftw3 is deprecated; use .convolve_fft instead', uplevel: 1
73
- options = operation_options(mode, boundary, fill_value, origin)
74
- convolve_fft(signal, kernel, **options)
86
+ # @raise (see convolve)
87
+ def correlate_fft(signal, kernel, mode: :valid, boundary: :constant,
88
+ fill_value: UNSPECIFIED_FILL, origin: 0)
89
+ execution(:correlation, signal, kernel, mode:, boundary:, fill_value:, origin:).fft
75
90
  end
76
91
 
77
- # Estimates the relative cost of {.convolve_fft} for the requested options.
78
- #
92
+ # Estimates the relative cost of {.convolve_fft}.
93
+ # This is a heuristic for algorithm selection, not a timing measurement.
94
+ # @param (see convolve)
79
95
  # @return [Float] machine-specific relative cost estimate
96
+ # @raise (see convolve)
80
97
  def predict_convolve_fft_time(signal, kernel, mode: :valid, boundary: :constant,
81
98
  fill_value: UNSPECIFIED_FILL, origin: 0)
82
- plan = operation_plan(signal, kernel, mode:, boundary:, fill_value:, origin:)
83
- transform_size = plan.wrap? ? signal.size : plan.linear_fft_size(kernel.shape)
84
- transform_cost = 16 * 4.55e-08 * transform_size * Math.log(transform_size)
85
- transform_cost + (4.55e-08 * fft_preparation_size(plan, signal, kernel))
99
+ execution(:convolution, signal, kernel, mode:, boundary:, fill_value:, origin:).fft_time
86
100
  end
87
101
 
88
- # Estimates the relative cost of {.convolve_basic} for the requested options.
89
- #
102
+ # Estimates the relative cost of {.correlate_fft}.
103
+ # This is a heuristic for algorithm selection, not a timing measurement.
104
+ # @param (see convolve)
90
105
  # @return [Float] machine-specific relative cost estimate
91
- def predict_convolve_basic_time(signal, kernel, mode: :valid, boundary: :constant,
92
- fill_value: UNSPECIFIED_FILL, origin: 0)
93
- plan = operation_plan(signal, kernel, mode:, boundary:, fill_value:, origin:)
94
- operations = plan.result_size * plan.extended_size * kernel.size
95
- operations += plan.extended_size unless plan.valid?
96
- 4.54e-12 * operations
97
- end
98
-
99
- private
100
-
101
- private :convolve_basic_valid
102
-
103
- def operation_plan(signal, kernel, mode:, boundary:, fill_value:, origin:)
104
- OperationPlan.new(signal, kernel, mode:, boundary:, fill_value:, origin:)
106
+ # @raise (see convolve)
107
+ def predict_correlate_fft_time(signal, kernel, mode: :valid, boundary: :constant,
108
+ fill_value: UNSPECIFIED_FILL, origin: 0)
109
+ execution(:correlation, signal, kernel, mode:, boundary:, fill_value:, origin:).fft_time
105
110
  end
106
111
 
107
- def operation_options(mode, boundary, fill_value, origin)
108
- options = { mode:, boundary:, origin: }
109
- options[:fill_value] = fill_value unless fill_value.equal?(UNSPECIFIED_FILL)
110
- options
111
- end
112
-
113
- def convolve_fft_valid(signal, kernel)
114
- ranges = kernel.shape.zip(signal.shape).map do |kernel_size, signal_size|
115
- (kernel_size - 1)...signal_size
116
- end
117
- full_convolution = Numo::Pocketfft.fftconvolve(signal, kernel.reverse)
118
-
119
- Numo::SFloat.cast(full_convolution[*ranges])
120
- end
121
-
122
- def convolve_fft_wrap(signal, kernel, plan)
123
- signal_value = Numo::DFloat.cast(signal)
124
- folded_kernel = fold_kernel(Numo::DFloat.cast(kernel), signal.shape, plan.anchors)
125
- spectrum = Numo::Pocketfft.fftn(signal_value) * Numo::Pocketfft.fftn(folded_kernel).conj
126
-
127
- Numo::SFloat.cast(Numo::Pocketfft.ifftn(spectrum).real)
112
+ # Estimates the relative cost of {.convolve_basic}.
113
+ # This is a heuristic for algorithm selection, not a timing measurement.
114
+ # @param (see convolve)
115
+ # @return [Float] machine-specific relative cost estimate
116
+ # @raise (see convolve)
117
+ def predict_convolve_basic_time(signal, kernel, mode: :valid, boundary: :constant,
118
+ fill_value: UNSPECIFIED_FILL, origin: 0)
119
+ execution(:convolution, signal, kernel, mode:, boundary:, fill_value:, origin:).basic_time
128
120
  end
129
121
 
130
- def fold_kernel(kernel, signal_shape, anchors)
131
- folded = Numo::DFloat.zeros(*signal_shape)
132
- kernel.flatten.each_with_index do |value, flat_index|
133
- target = folded_kernel_target(flat_index, kernel.shape, signal_shape, anchors)
134
- folded[*target] = folded[*target] + value
135
- end
136
- folded
122
+ # Estimates the relative cost of {.correlate_basic}.
123
+ # This is a heuristic for algorithm selection, not a timing measurement.
124
+ # @param (see convolve)
125
+ # @return [Float] machine-specific relative cost estimate
126
+ # @raise (see convolve)
127
+ def predict_correlate_basic_time(signal, kernel, mode: :valid, boundary: :constant,
128
+ fill_value: UNSPECIFIED_FILL, origin: 0)
129
+ execution(:correlation, signal, kernel, mode:, boundary:, fill_value:, origin:).basic_time
137
130
  end
138
131
 
139
- def folded_kernel_target(flat_index, kernel_shape, signal_shape, anchors)
140
- remainder = flat_index
141
- Array.new(kernel_shape.length).tap do |target|
142
- (kernel_shape.length - 1).downto(0) do |axis|
143
- coordinate = remainder % kernel_shape[axis]
144
- remainder /= kernel_shape[axis]
145
- target[axis] = (coordinate - anchors[axis]) % signal_shape[axis]
146
- end
147
- end
148
- end
132
+ private
149
133
 
150
- def fft_preparation_size(plan, signal, kernel)
151
- return signal.size + kernel.size if plan.wrap?
152
- return 0 if plan.valid?
134
+ private :convolve_basic_valid, :correlate_basic_valid
153
135
 
154
- plan.extended_size
136
+ def execution(operation, signal, kernel, mode:, boundary:, fill_value:, origin:)
137
+ OperationExecution.new(operation, signal, kernel, mode:, boundary:, fill_value:, origin:)
155
138
  end
156
139
  end
157
140
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: convolver
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil Slater
@@ -18,7 +18,7 @@ dependencies:
18
18
  version: 0.9.9
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
- version: '0.11'
21
+ version: '0.12'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -28,14 +28,14 @@ dependencies:
28
28
  version: 0.9.9
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
- version: '0.11'
31
+ version: '0.12'
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: numo-pocketfft
34
34
  requirement: !ruby/object:Gem::Requirement
35
35
  requirements:
36
36
  - - ">="
37
37
  - !ruby/object:Gem::Version
38
- version: '0.6'
38
+ version: 0.7.0
39
39
  - - "<"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0.8'
@@ -45,12 +45,12 @@ dependencies:
45
45
  requirements:
46
46
  - - ">="
47
47
  - !ruby/object:Gem::Version
48
- version: '0.6'
48
+ version: 0.7.0
49
49
  - - "<"
50
50
  - !ruby/object:Gem::Version
51
51
  version: '0.8'
52
- description: Fast cross-correlation for multidimensional Numo::NArray values, with
53
- configurable output and boundary modes.
52
+ description: Fast mathematical convolution and cross-correlation for multidimensional
53
+ Numo::NArray values, with configurable output and boundary modes.
54
54
  email:
55
55
  - slobo777@gmail.com
56
56
  executables: []
@@ -64,11 +64,22 @@ files:
64
64
  - ext/convolver/convolve_raw.c
65
65
  - ext/convolver/convolve_raw.h
66
66
  - ext/convolver/convolver.c
67
+ - ext/convolver/correlate_raw.c
68
+ - ext/convolver/correlate_raw.h
67
69
  - ext/convolver/extconf.rb
68
70
  - lib/convolver.rb
71
+ - lib/convolver/buffer_limits.rb
72
+ - lib/convolver/buffer_preparation.rb
73
+ - lib/convolver/circular_fft_operation.rb
74
+ - lib/convolver/fft_buffer_plan.rb
75
+ - lib/convolver/fft_estimator.rb
76
+ - lib/convolver/fft_limits.rb
77
+ - lib/convolver/linear_fft_operation.rb
78
+ - lib/convolver/operation_execution.rb
69
79
  - lib/convolver/operation_options.rb
70
80
  - lib/convolver/operation_plan.rb
71
81
  - lib/convolver/operation_shapes.rb
82
+ - lib/convolver/real_fft_shape.rb
72
83
  - lib/convolver/signal_extension.rb
73
84
  - lib/convolver/version.rb
74
85
  homepage: https://github.com/neilslater/convolver
@@ -93,5 +104,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
104
  requirements: []
94
105
  rubygems_version: 4.0.16
95
106
  specification_version: 4
96
- summary: Fast cross-correlation for Numo::NArray
107
+ summary: Fast convolution and cross-correlation for Numo::NArray
97
108
  test_files: []