convolver 1.0.1 → 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 +82 -0
- data/README.md +153 -36
- data/ext/convolver/convolve_raw.c +11 -9
- data/ext/convolver/convolve_raw.h +3 -0
- data/ext/convolver/convolver.c +69 -25
- 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 +118 -0
- data/lib/convolver/operation_plan.rb +63 -0
- data/lib/convolver/operation_shapes.rb +129 -0
- data/lib/convolver/real_fft_shape.rb +135 -0
- data/lib/convolver/signal_extension.rb +67 -0
- data/lib/convolver/version.rb +1 -1
- data/lib/convolver.rb +102 -71
- metadata +24 -9
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// ext/convolver/correlate_raw.c
|
|
2
|
+
|
|
3
|
+
#include "correlate_raw.h"
|
|
4
|
+
|
|
5
|
+
static inline size_t checked_add( size_t left, size_t right, const char *description ) {
|
|
6
|
+
if ( right > SIZE_MAX - left ) {
|
|
7
|
+
rb_raise( rb_eRangeError, "%s exceeds native implementation limit", description );
|
|
8
|
+
}
|
|
9
|
+
return left + right;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static inline size_t checked_multiply( size_t left, size_t right, const char *description ) {
|
|
13
|
+
if ( left != 0 && right > SIZE_MAX / left ) {
|
|
14
|
+
rb_raise( rb_eRangeError, "%s exceeds native implementation limit", description );
|
|
15
|
+
}
|
|
16
|
+
return left * right;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static inline size_t size_from_shape( int rank, const size_t *shape, const char *description ) {
|
|
20
|
+
size_t size = 1;
|
|
21
|
+
int i;
|
|
22
|
+
for ( i = 0; i < rank; i++ ) {
|
|
23
|
+
size = checked_multiply( size, shape[i], description );
|
|
24
|
+
}
|
|
25
|
+
return size;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Sets reverse indices
|
|
29
|
+
static inline void corner_reset( int rank, const size_t *shape, size_t *rev_indices ) {
|
|
30
|
+
int i;
|
|
31
|
+
for ( i = 0; i < rank; i++ ) { rev_indices[i] = shape[i] - 1; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Counts indices down, returns number of ranks that reset
|
|
35
|
+
static inline int corner_dec( int rank, const size_t *shape, size_t *rev_indices ) {
|
|
36
|
+
int i = 0;
|
|
37
|
+
(void) rank;
|
|
38
|
+
while ( ! rev_indices[i]-- ) {
|
|
39
|
+
rev_indices[i] = shape[i] - 1;
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
return i;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Generates co-increment steps by rank boundaries crossed, for the outer position as inner position is incremented by 1
|
|
46
|
+
static inline void calc_co_increment(
|
|
47
|
+
int rank, const size_t *outer_shape, const size_t *inner_shape, size_t *co_increment ) {
|
|
48
|
+
size_t factor = 1;
|
|
49
|
+
int i;
|
|
50
|
+
co_increment[0] = 1; // co-increment is always 1 in lowest rank
|
|
51
|
+
for ( i = 0; i < rank; i++ ) {
|
|
52
|
+
size_t skipped = checked_multiply( factor, outer_shape[i] - inner_shape[i], "array offset" );
|
|
53
|
+
co_increment[i+1] = checked_add( co_increment[i], skipped, "array offset" );
|
|
54
|
+
factor = checked_multiply( factor, outer_shape[i], "array size" );
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static inline size_t maximum_offset(
|
|
59
|
+
int rank, const size_t *outer_shape, const size_t *inner_shape, const char *description ) {
|
|
60
|
+
size_t factor = 1;
|
|
61
|
+
size_t offset = 0;
|
|
62
|
+
int i;
|
|
63
|
+
for ( i = 0; i < rank; i++ ) {
|
|
64
|
+
size_t dimension_offset = checked_multiply( factor, inner_shape[i] - 1, description );
|
|
65
|
+
offset = checked_add( offset, dimension_offset, description );
|
|
66
|
+
factor = checked_multiply( factor, outer_shape[i], "array size" );
|
|
67
|
+
}
|
|
68
|
+
return offset;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
72
|
+
//
|
|
73
|
+
// Correlate
|
|
74
|
+
//
|
|
75
|
+
|
|
76
|
+
void correlate_raw(
|
|
77
|
+
int in_rank, const size_t *in_shape, const float *in_ptr,
|
|
78
|
+
int kernel_rank, const size_t *kernel_shape, const float *kernel_ptr,
|
|
79
|
+
int out_rank, const size_t *out_shape, float *out_ptr ) {
|
|
80
|
+
size_t i, j, input_size, kernel_size, kernel_aligned, out_size, offset;
|
|
81
|
+
size_t maximum_input_offset;
|
|
82
|
+
size_t out_co_incr[LARGEST_RANK + 1], kernel_co_incr[LARGEST_RANK + 1];
|
|
83
|
+
size_t ker_q[LARGEST_RANK], out_q[LARGEST_RANK];
|
|
84
|
+
size_t *kernel_co_incr_cache;
|
|
85
|
+
VALUE cache_storage = 0;
|
|
86
|
+
|
|
87
|
+
kernel_size = size_from_shape( kernel_rank, kernel_shape, "kernel size" );
|
|
88
|
+
kernel_aligned = kernel_size - kernel_size % 4;
|
|
89
|
+
out_size = size_from_shape( out_rank, out_shape, "output size" );
|
|
90
|
+
input_size = size_from_shape( in_rank, in_shape, "input size" );
|
|
91
|
+
|
|
92
|
+
calc_co_increment( in_rank, in_shape, out_shape, out_co_incr );
|
|
93
|
+
calc_co_increment( in_rank, in_shape, kernel_shape, kernel_co_incr );
|
|
94
|
+
maximum_input_offset = checked_add(
|
|
95
|
+
maximum_offset( in_rank, in_shape, out_shape, "input offset" ),
|
|
96
|
+
maximum_offset( in_rank, in_shape, kernel_shape, "input offset" ),
|
|
97
|
+
"input offset"
|
|
98
|
+
);
|
|
99
|
+
if ( maximum_input_offset >= input_size ) {
|
|
100
|
+
rb_raise( rb_eRangeError, "input offset exceeds native implementation limit" );
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
kernel_co_incr_cache = RB_ALLOCV_N( size_t, cache_storage, kernel_size );
|
|
104
|
+
kernel_co_incr_cache[0] = 0;
|
|
105
|
+
|
|
106
|
+
corner_reset( kernel_rank, kernel_shape, ker_q );
|
|
107
|
+
for ( i = 1; i < kernel_size; i++ ) {
|
|
108
|
+
kernel_co_incr_cache[i] = checked_add(
|
|
109
|
+
kernel_co_incr_cache[i-1],
|
|
110
|
+
kernel_co_incr[ corner_dec( kernel_rank, kernel_shape, ker_q ) ],
|
|
111
|
+
"kernel offset"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
offset = 0;
|
|
116
|
+
corner_reset( out_rank, out_shape, out_q );
|
|
117
|
+
|
|
118
|
+
// Main correlation loop
|
|
119
|
+
for ( i = 0; i < out_size; i++ ) {
|
|
120
|
+
#if CONVOLVER_USE_SSE
|
|
121
|
+
__m128 simd_x, simd_y, simd_t;
|
|
122
|
+
float v[4];
|
|
123
|
+
simd_t = _mm_setzero_ps();
|
|
124
|
+
#endif
|
|
125
|
+
float t = 0.0;
|
|
126
|
+
|
|
127
|
+
#if CONVOLVER_USE_SSE
|
|
128
|
+
// Contiguous kernel views need not start on a 16-byte boundary.
|
|
129
|
+
for ( j = 0; j < kernel_aligned; j +=4 ) {
|
|
130
|
+
simd_x = _mm_loadu_ps( kernel_ptr + j );
|
|
131
|
+
// Yes the backwards alignment is correct
|
|
132
|
+
simd_y = _mm_set_ps( in_ptr[ offset + kernel_co_incr_cache[j+3] ], in_ptr[ offset + kernel_co_incr_cache[j+2] ],
|
|
133
|
+
in_ptr[ offset + kernel_co_incr_cache[j+1] ], in_ptr[ offset + kernel_co_incr_cache[j] ] );
|
|
134
|
+
simd_x = _mm_mul_ps( simd_x, simd_y );
|
|
135
|
+
simd_t = _mm_add_ps( simd_x, simd_t );
|
|
136
|
+
}
|
|
137
|
+
_mm_store_ps( v, simd_t );
|
|
138
|
+
#else
|
|
139
|
+
// SSE is unavailable on architectures such as Apple Silicon.
|
|
140
|
+
// Use the same calculation without x86-specific intrinsics.
|
|
141
|
+
for ( j = 0; j < kernel_aligned; j++ ) {
|
|
142
|
+
t += in_ptr[ offset + kernel_co_incr_cache[j] ] * kernel_ptr[j];
|
|
143
|
+
}
|
|
144
|
+
#endif
|
|
145
|
+
|
|
146
|
+
// Complete any remaining 1,2 or 3 items one at a time
|
|
147
|
+
for ( j = kernel_aligned; j < kernel_size; j++ ) {
|
|
148
|
+
t += in_ptr[ offset + kernel_co_incr_cache[j] ] * kernel_ptr[ j ];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#if CONVOLVER_USE_SSE
|
|
152
|
+
out_ptr[i] = v[0] + v[1] + v[2] + v[3] + t;
|
|
153
|
+
#else
|
|
154
|
+
out_ptr[i] = t;
|
|
155
|
+
#endif
|
|
156
|
+
|
|
157
|
+
if ( i + 1 < out_size ) {
|
|
158
|
+
offset = checked_add(
|
|
159
|
+
offset,
|
|
160
|
+
out_co_incr[ corner_dec( out_rank, out_shape, out_q ) ],
|
|
161
|
+
"input offset"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
RB_ALLOCV_END( cache_storage );
|
|
167
|
+
}
|
|
@@ -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
|