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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -1
- data/README.md +99 -51
- data/ext/convolver/convolve_raw.c +11 -9
- data/ext/convolver/convolve_raw.h +3 -0
- data/ext/convolver/convolver.c +68 -24
- 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 +11 -7
- data/lib/convolver/operation_plan.rb +22 -7
- data/lib/convolver/operation_shapes.rb +42 -9
- data/lib/convolver/real_fft_shape.rb +135 -0
- data/lib/convolver/signal_extension.rb +1 -0
- data/lib/convolver/version.rb +1 -1
- data/lib/convolver.rb +91 -108
- metadata +19 -8
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// ext/convolver/correlate_raw.h
|
|
2
|
+
|
|
3
|
+
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
4
|
+
//
|
|
5
|
+
// Declarations of narray helper functions
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#ifndef CORRELATE_RAW_H
|
|
9
|
+
#define CORRELATE_RAW_H
|
|
10
|
+
|
|
11
|
+
#ifndef CONVOLVER_RAW_CONFIG
|
|
12
|
+
#define CONVOLVER_RAW_CONFIG
|
|
13
|
+
#include <ruby.h>
|
|
14
|
+
#if defined(__SSE__)
|
|
15
|
+
#include <xmmintrin.h>
|
|
16
|
+
#define CONVOLVER_USE_SSE 1
|
|
17
|
+
#else
|
|
18
|
+
#define CONVOLVER_USE_SSE 0
|
|
19
|
+
#endif
|
|
20
|
+
#define LARGEST_RANK 16
|
|
21
|
+
#endif
|
|
22
|
+
|
|
23
|
+
void correlate_raw(
|
|
24
|
+
int in_rank, const size_t *in_shape, const float *in_ptr,
|
|
25
|
+
int kernel_rank, const size_t *kernel_shape, const float *kernel_ptr,
|
|
26
|
+
int out_rank, const size_t *out_shape, float *out_ptr );
|
|
27
|
+
|
|
28
|
+
#endif
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Checks individual allocations against native size and tagged-stride limits.
|
|
5
|
+
# @private
|
|
6
|
+
class BufferLimits
|
|
7
|
+
SIZE_MAX = (1 << ([0].pack('J').bytesize * 8)) - 1
|
|
8
|
+
# Numo tags signed strides with a left shift: floor(SSIZE_MAX / 2).
|
|
9
|
+
BYTE_MAX = SIZE_MAX / 4
|
|
10
|
+
INT_MAX = (1 << (([0].pack('i').bytesize * 8) - 1)) - 1
|
|
11
|
+
INDEX_BYTES = [0].pack('J').bytesize
|
|
12
|
+
|
|
13
|
+
attr_reader :size_max, :byte_max, :int_max
|
|
14
|
+
|
|
15
|
+
def initialize(size_max: SIZE_MAX, byte_max: BYTE_MAX, int_max: INT_MAX)
|
|
16
|
+
@size_max = size_max
|
|
17
|
+
@byte_max = byte_max
|
|
18
|
+
@int_max = int_max
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def buffer!(count, width, description, error: RangeError)
|
|
22
|
+
return if count <= byte_max / width
|
|
23
|
+
|
|
24
|
+
raise error, "#{description} exceeds native byte/stride limit"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def product!(shape, description, error: RangeError)
|
|
28
|
+
shape.reduce(1) do |product, size|
|
|
29
|
+
raise error, "#{description} exceeds native element limit" if size > size_max / product
|
|
30
|
+
|
|
31
|
+
product * size
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
NATIVE = new.freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private_constant :BufferLimits
|
|
39
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'convolver/fft_limits'
|
|
4
|
+
|
|
5
|
+
module Convolver
|
|
6
|
+
# Preflights casts, extension, indexing and direct-path cache allocations.
|
|
7
|
+
# @private
|
|
8
|
+
class BufferPreparation
|
|
9
|
+
def initialize(plan, signal_shape, kernel_shape, limits: BufferLimits::NATIVE)
|
|
10
|
+
@plan = plan
|
|
11
|
+
@signal_size = limits.product!(signal_shape, 'signal size')
|
|
12
|
+
@kernel_size = limits.product!(kernel_shape, 'kernel size')
|
|
13
|
+
@limits = limits
|
|
14
|
+
@scalar = signal_shape.empty?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def basic!
|
|
18
|
+
common!
|
|
19
|
+
extension!
|
|
20
|
+
limits.buffer!(kernel_size, BufferLimits::INDEX_BYTES, 'direct kernel offset cache')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fft!
|
|
24
|
+
common!
|
|
25
|
+
return if @scalar
|
|
26
|
+
return extension! unless plan.wrap?
|
|
27
|
+
|
|
28
|
+
limits.buffer!(signal_size, 8, 'circular signal preparation', error: FftUnavailable)
|
|
29
|
+
limits.buffer!(kernel_size, 8, 'circular kernel preparation', error: FftUnavailable)
|
|
30
|
+
limits.buffer!(kernel_size, BufferLimits::INDEX_BYTES, 'circular kernel index buffer', error: FftUnavailable)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :plan, :signal_size, :kernel_size, :limits
|
|
36
|
+
|
|
37
|
+
def common!
|
|
38
|
+
limits.buffer!(plan.result_size, 4, 'result buffer')
|
|
39
|
+
limits.buffer!(signal_size, 4, 'signal conversion buffer')
|
|
40
|
+
limits.buffer!(kernel_size, 4, 'kernel conversion buffer')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def extension!
|
|
44
|
+
return if plan.valid?
|
|
45
|
+
|
|
46
|
+
limits.buffer!(plan.extended_size, 4, 'extended signal buffer')
|
|
47
|
+
return if plan.boundary == :constant
|
|
48
|
+
|
|
49
|
+
plan.extended_shape.each do |length|
|
|
50
|
+
limits.buffer!(length, BufferLimits::INDEX_BYTES, 'boundary index buffer')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private_constant :BufferPreparation
|
|
56
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Calculates same-sized periodic operations through PocketFFT transforms.
|
|
5
|
+
# @private
|
|
6
|
+
class CircularFftOperation
|
|
7
|
+
def initialize(operation, signal, kernel, plan)
|
|
8
|
+
@real_axis = plan.fft_buffers.real_axis
|
|
9
|
+
@operation = operation
|
|
10
|
+
@signal = Numo::DFloat.cast(signal)
|
|
11
|
+
@kernel = fold_kernel(Numo::DFloat.cast(kernel), signal.shape, plan.anchors)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
return complex_product unless real_axis
|
|
16
|
+
|
|
17
|
+
real_product(real_axis)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.real_axis(shape)
|
|
21
|
+
return nil if shape.empty?
|
|
22
|
+
|
|
23
|
+
final_axis = shape.length - 1
|
|
24
|
+
return final_axis if shape[final_axis].even?
|
|
25
|
+
|
|
26
|
+
shape.each_index.select { |axis| shape[axis].even? }.max_by { |axis| shape[axis] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :operation, :signal, :kernel, :real_axis
|
|
32
|
+
|
|
33
|
+
def real_product(real_axis)
|
|
34
|
+
final_axis = signal.ndim - 1
|
|
35
|
+
prepared_signal = prepare_real_operand(signal, real_axis, final_axis)
|
|
36
|
+
prepared_kernel = prepare_real_operand(kernel, real_axis, final_axis)
|
|
37
|
+
result = real_spectrum_product(prepared_signal, prepared_kernel)
|
|
38
|
+
result = result.swapaxes(real_axis, final_axis) unless real_axis == final_axis
|
|
39
|
+
Numo::SFloat.cast(result)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def prepare_real_operand(value, real_axis, final_axis)
|
|
43
|
+
real_axis == final_axis ? value : value.swapaxes(real_axis, final_axis)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def real_spectrum_product(signal_value, kernel_value)
|
|
47
|
+
signal_spectrum = Numo::Pocketfft.rfftn(signal_value)
|
|
48
|
+
kernel_spectrum = Numo::Pocketfft.rfftn(kernel_value)
|
|
49
|
+
kernel_spectrum.inplace.conj if operation == :correlation
|
|
50
|
+
Numo::Pocketfft.irfftn(signal_spectrum.inplace * kernel_spectrum)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def complex_product
|
|
54
|
+
signal_spectrum = Numo::Pocketfft.fftn(signal)
|
|
55
|
+
kernel_spectrum = Numo::Pocketfft.fftn(kernel)
|
|
56
|
+
kernel_spectrum.inplace.conj if operation == :correlation
|
|
57
|
+
result = Numo::Pocketfft.ifftn(signal_spectrum.inplace * kernel_spectrum).real
|
|
58
|
+
Numo::SFloat.cast(result)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def fold_kernel(source, signal_shape, anchors)
|
|
62
|
+
folded = Numo::DFloat.zeros(*signal_shape)
|
|
63
|
+
source.flatten.each_with_index do |value, flat_index|
|
|
64
|
+
target = folded_target(flat_index, source.shape, signal_shape, anchors)
|
|
65
|
+
folded[*target] = folded[*target] + value
|
|
66
|
+
end
|
|
67
|
+
folded
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def folded_target(flat_index, kernel_shape, signal_shape, anchors)
|
|
71
|
+
remainder = flat_index
|
|
72
|
+
Array.new(kernel_shape.length).tap do |target|
|
|
73
|
+
(kernel_shape.length - 1).downto(0) do |axis|
|
|
74
|
+
coordinate = remainder % kernel_shape[axis]
|
|
75
|
+
remainder /= kernel_shape[axis]
|
|
76
|
+
target[axis] = (coordinate - anchors[axis]) % signal_shape[axis]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private_constant :CircularFftOperation
|
|
83
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'convolver/fft_limits'
|
|
4
|
+
require 'convolver/buffer_preparation'
|
|
5
|
+
|
|
6
|
+
module Convolver
|
|
7
|
+
# Shares allocation-free FFT dimensions between execution and estimation.
|
|
8
|
+
# @private
|
|
9
|
+
class FftBufferPlan
|
|
10
|
+
attr_reader :transform_shape, :real_axis, :transform_size, :spectrum_size
|
|
11
|
+
|
|
12
|
+
def initialize(plan, signal_shape, kernel_shape, limits: BufferLimits::NATIVE)
|
|
13
|
+
BufferPreparation.new(plan, signal_shape, kernel_shape, limits:).fft!
|
|
14
|
+
@real_axis = plan.wrap? ? CircularFftOperation.real_axis(signal_shape) : signal_shape.length - 1
|
|
15
|
+
@transform_shape = select_shape(plan, signal_shape, kernel_shape, FftLimits.new(limits))
|
|
16
|
+
@transform_size = transform_shape.reduce(1, :*)
|
|
17
|
+
@spectrum_size = spectrum_elements
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def select_shape(plan, signal_shape, kernel_shape, limits)
|
|
23
|
+
return [].freeze if signal_shape.empty?
|
|
24
|
+
return plan.linear_fft_shape(kernel_shape, limits:) unless plan.wrap?
|
|
25
|
+
|
|
26
|
+
limits.validate!(signal_shape, real_axis:)
|
|
27
|
+
signal_shape.dup.freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def spectrum_elements
|
|
31
|
+
return transform_size if transform_shape.empty? || real_axis.nil?
|
|
32
|
+
|
|
33
|
+
length = transform_shape[real_axis]
|
|
34
|
+
transform_size / length * ((length / 2) + 1)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private_constant :FftBufferPlan
|
|
39
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Estimates the selected real or complex PocketFFT implementation cost.
|
|
5
|
+
# @private
|
|
6
|
+
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
|
+
AXIS_MOVE_COST = 1.6e-9
|
|
12
|
+
CORRELATION_COST = 2.0e-10
|
|
13
|
+
|
|
14
|
+
def initialize(operation, signal, kernel, plan)
|
|
15
|
+
@operation = operation
|
|
16
|
+
@signal = signal
|
|
17
|
+
@kernel = kernel
|
|
18
|
+
@plan = plan
|
|
19
|
+
@buffers = plan.fft_buffers
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
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
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :operation, :signal, :kernel, :plan, :buffers
|
|
34
|
+
|
|
35
|
+
def dimensions
|
|
36
|
+
return linear_dimensions unless plan.wrap?
|
|
37
|
+
|
|
38
|
+
circular_dimensions
|
|
39
|
+
end
|
|
40
|
+
|
|
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)]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def linear_dimensions
|
|
47
|
+
[buffers.transform_size, buffers.spectrum_size,
|
|
48
|
+
cost_for(REAL_FFT_COST), 0]
|
|
49
|
+
end
|
|
50
|
+
|
|
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
|
|
56
|
+
|
|
57
|
+
def cost_for(costs)
|
|
58
|
+
costs.fetch(signal.ndim, costs.values.last)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def transform_coefficient(real_axis)
|
|
62
|
+
cost_for(real_axis ? REAL_FFT_COST : COMPLEX_FFT_COST)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def moved_size(real_axis)
|
|
66
|
+
real_axis && real_axis != signal.ndim - 1 ? 3 * signal.size : 0
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private_constant :FftEstimator
|
|
71
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'convolver/buffer_limits'
|
|
4
|
+
|
|
5
|
+
# Internal allocation and arithmetic checks for the PocketFFT paths.
|
|
6
|
+
module Convolver
|
|
7
|
+
# Only FFT representability failures permit automatic direct fallback.
|
|
8
|
+
# @private
|
|
9
|
+
class FftUnavailable < RangeError; end
|
|
10
|
+
|
|
11
|
+
# Bounds PocketFFT buffers and signed arithmetic without constructing plans.
|
|
12
|
+
# @private
|
|
13
|
+
class FftLimits
|
|
14
|
+
def initialize(buffers = BufferLimits::NATIVE)
|
|
15
|
+
@buffers = buffers
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate!(shape, real_axis:)
|
|
19
|
+
total = buffers.product!(shape, 'FFT size', error: FftUnavailable)
|
|
20
|
+
validate_buffers!(shape, total, real_axis)
|
|
21
|
+
# The wrapper casts total to int before dividing it by the axis length.
|
|
22
|
+
integer!(total, buffers.int_max, 'FFT total size')
|
|
23
|
+
# Real forward computes int z_step = (z / 2 + 1) * 2.
|
|
24
|
+
integer!(shape[real_axis], buffers.int_max - 2, 'FFT real stride') if real_axis
|
|
25
|
+
validate_axes!(shape)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :buffers
|
|
31
|
+
|
|
32
|
+
def validate_axes!(shape)
|
|
33
|
+
# FFTPACK twiddle counts are <= 2n complex / 3n real. Bluestein m < 4n,
|
|
34
|
+
# so each variable allocation is <= 128n bytes, including inner twiddles.
|
|
35
|
+
# The 16n int bound also protects signed intermediates at the inner m.
|
|
36
|
+
shape.each do |length|
|
|
37
|
+
integer!(length, buffers.int_max / 16, 'FFT axis arithmetic')
|
|
38
|
+
buffer!(length, 128, 'FFT axis work buffer')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate_buffers!(shape, total, real_axis)
|
|
43
|
+
buffer!(total, 8, 'FFT spatial buffer')
|
|
44
|
+
return buffer!(total, 16, 'FFT complex buffer') unless real_axis
|
|
45
|
+
|
|
46
|
+
spectrum = total / shape[real_axis] * ((shape[real_axis] / 2) + 1)
|
|
47
|
+
buffer!(spectrum, 16, 'FFT spectrum buffer')
|
|
48
|
+
buffer!(total, 16, 'FFT inverse staging buffer')
|
|
49
|
+
buffer!(total, 8, 'FFT inverse output buffer')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def buffer!(count, width, description)
|
|
53
|
+
buffers.buffer!(count, width, description, error: FftUnavailable)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def integer!(value, maximum, description)
|
|
57
|
+
return if value <= maximum
|
|
58
|
+
|
|
59
|
+
raise FftUnavailable, "#{description} exceeds native integer limit"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private_constant :FftUnavailable, :FftLimits
|
|
64
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Convolver
|
|
4
|
+
# Calculates a valid linear operation through real PocketFFT transforms.
|
|
5
|
+
# @private
|
|
6
|
+
class LinearFftOperation
|
|
7
|
+
def initialize(operation, signal, kernel, plan)
|
|
8
|
+
@operation = operation
|
|
9
|
+
@signal = signal
|
|
10
|
+
@kernel = kernel
|
|
11
|
+
@transform_shape = plan.fft_buffers.transform_shape
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
signal_padded = zero_padded(signal)
|
|
16
|
+
kernel_padded = zero_padded(kernel)
|
|
17
|
+
full_result = real_spectrum_product(signal_padded, kernel_padded)
|
|
18
|
+
Numo::SFloat.cast(full_result[*valid_ranges])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :operation, :signal, :kernel, :transform_shape
|
|
24
|
+
|
|
25
|
+
def zero_padded(value)
|
|
26
|
+
Numo::DFloat.zeros(*transform_shape).tap do |padded|
|
|
27
|
+
padded[*value.shape.map { |size| 0...size }] = value
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def real_spectrum_product(signal_value, kernel_value)
|
|
32
|
+
signal_spectrum = Numo::Pocketfft.rfftn(signal_value)
|
|
33
|
+
kernel_spectrum = Numo::Pocketfft.rfftn(kernel_value)
|
|
34
|
+
kernel_spectrum.inplace.conj if operation == :correlation
|
|
35
|
+
Numo::Pocketfft.irfftn(signal_spectrum.inplace * kernel_spectrum)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def valid_ranges
|
|
39
|
+
signal.shape.zip(kernel.shape).map do |signal_size, kernel_size|
|
|
40
|
+
if operation == :correlation
|
|
41
|
+
0...(signal_size - kernel_size + 1)
|
|
42
|
+
else
|
|
43
|
+
(kernel_size - 1)...signal_size
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private_constant :LinearFftOperation
|
|
50
|
+
end
|
|
@@ -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
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Convolver
|
|
4
|
-
# Validates and normalizes
|
|
4
|
+
# Validates and normalizes one convolution or correlation operation.
|
|
5
|
+
# @private
|
|
5
6
|
class OperationOptions
|
|
7
|
+
OPERATIONS = %i[convolution correlation].freeze
|
|
6
8
|
MODES = %i[valid same full].freeze
|
|
7
9
|
BOUNDARIES = %i[constant nearest reflect mirror wrap].freeze
|
|
8
10
|
|
|
9
|
-
attr_reader :mode, :boundary, :fill_value, :origins, :anchors
|
|
11
|
+
attr_reader :operation, :mode, :boundary, :fill_value, :origins, :anchors
|
|
10
12
|
|
|
11
|
-
def initialize(signal, kernel, mode:, boundary:, fill_value:, origin:)
|
|
13
|
+
def initialize(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
|
|
12
14
|
validate_inputs!(signal, kernel)
|
|
13
|
-
validate_vocabulary!(mode, boundary)
|
|
14
|
-
assign_options(signal, kernel, mode:, boundary:, fill_value:, origin:)
|
|
15
|
+
validate_vocabulary!(operation, mode, boundary)
|
|
16
|
+
assign_options(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
|
|
15
17
|
validate_combinations!(signal.shape, kernel.shape)
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -26,14 +28,16 @@ module Convolver
|
|
|
26
28
|
raise ArgumentError, "maximum supported rank is #{MAX_RANK}" if signal.ndim > MAX_RANK
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def validate_vocabulary!(mode, boundary)
|
|
31
|
+
def validate_vocabulary!(operation, mode, boundary)
|
|
32
|
+
raise ArgumentError, "operation must be one of #{OPERATIONS.inspect}" unless OPERATIONS.include?(operation)
|
|
30
33
|
raise ArgumentError, "mode must be one of #{MODES.inspect}" unless MODES.include?(mode)
|
|
31
34
|
return if BOUNDARIES.include?(boundary)
|
|
32
35
|
|
|
33
36
|
raise ArgumentError, "boundary must be one of #{BOUNDARIES.inspect}"
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
def assign_options(signal, kernel, mode:, boundary:, fill_value:, origin:)
|
|
39
|
+
def assign_options(signal, kernel, operation:, mode:, boundary:, fill_value:, origin:)
|
|
40
|
+
@operation = operation
|
|
37
41
|
@mode = mode
|
|
38
42
|
@boundary = boundary
|
|
39
43
|
@fill_value_given = !fill_value.equal?(UNSPECIFIED_FILL)
|
|
@@ -4,25 +4,32 @@ require 'forwardable'
|
|
|
4
4
|
require 'convolver/operation_options'
|
|
5
5
|
require 'convolver/operation_shapes'
|
|
6
6
|
require 'convolver/signal_extension'
|
|
7
|
+
require 'convolver/fft_buffer_plan'
|
|
7
8
|
|
|
8
9
|
# Internal planning and extension support for Convolver's public operations.
|
|
9
10
|
module Convolver
|
|
10
11
|
# Distinguishes an omitted fill_value keyword from an explicitly supplied
|
|
11
12
|
# value. This lets non-constant boundaries reject even an explicit zero.
|
|
13
|
+
# @private
|
|
12
14
|
UNSPECIFIED_FILL = Object.new.freeze
|
|
13
15
|
|
|
14
|
-
# Validated dimensions and boundary-extension details for one
|
|
16
|
+
# Validated dimensions and boundary-extension details for one operation.
|
|
17
|
+
# @private
|
|
15
18
|
class OperationPlan
|
|
16
19
|
extend Forwardable
|
|
17
20
|
|
|
18
|
-
def_delegators :options, :mode, :boundary, :fill_value, :origins, :anchors
|
|
21
|
+
def_delegators :options, :operation, :mode, :boundary, :fill_value, :origins, :anchors
|
|
19
22
|
def_delegators :shapes, :padding_before, :padding_after, :result_shape,
|
|
20
23
|
:extended_shape, :result_size, :extended_size, :linear_fft_shape,
|
|
21
|
-
:linear_fft_size
|
|
22
|
-
|
|
23
|
-
def initialize(signal, kernel, mode:, boundary:, fill_value:, origin:)
|
|
24
|
-
@options = OperationOptions.new(signal, kernel, mode:, boundary:, fill_value:, origin:)
|
|
25
|
-
@
|
|
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
|
+
)
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
def valid?
|
|
@@ -39,6 +46,14 @@ module Convolver
|
|
|
39
46
|
SignalExtension.new(shapes, boundary:, fill_value:).call(signal)
|
|
40
47
|
end
|
|
41
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
|
+
|
|
42
57
|
private
|
|
43
58
|
|
|
44
59
|
attr_reader :options, :shapes
|