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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e56b4c0d87d92abb3f8c79d3d11b8d41c8dcbb6d7055dce615eef07a363b0cc2
|
|
4
|
+
data.tar.gz: 23c852764b162f082fa380e6932a8c5ad0c400c6d766579d7a087d32cd875825
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7eda50980613f1d03659465daa5b3ece647485ab38355664808497f966f232893de2133649c0e8cd9262606fde99ad3305552db27a5e719901aee6c095b2ac3d
|
|
7
|
+
data.tar.gz: da94b103db3c1af9f735787584a757ed9683a12556879495f041a88a6f7eb178ef214d871f64e8a18aa498b090c9cec748c4c7d11c534a687099e9d2a3c15ff2
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.0.1] - 2026-10-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Require `numo-pocketfft >= 0.7.0` and allow `numo-narray-alt < 0.12`,
|
|
15
|
+
enabling the NArray 0.11 series.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Read the logical values of contiguous signal and kernel slices in direct
|
|
20
|
+
convolution and correlation, including unaligned SSE kernels and scalar views.
|
|
21
|
+
- Retain a representable exact FFT shape when an optional fast candidate exceeds
|
|
22
|
+
the native element limit.
|
|
23
|
+
- Reject unrepresentable FFT buffers and native integer arithmetic before
|
|
24
|
+
allocation, including preparation, inverse staging and work buffers. Automatic
|
|
25
|
+
selection retains a safe direct path when FFT exceeds its limits.
|
|
26
|
+
|
|
27
|
+
## [3.0.0] - 2026-08-22
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Added the `correlate`, `correlate_basic`, and `correlate_fft` calculation
|
|
32
|
+
methods for cross-correlation, preserving the numerical operation exposed by
|
|
33
|
+
the version 2 `convolve*` methods.
|
|
34
|
+
- Added `predict_correlate_basic_time` and `predict_correlate_fft_time` as the
|
|
35
|
+
corresponding estimator family.
|
|
36
|
+
- Added distinct native correlation and convolution primitives whose hot loops
|
|
37
|
+
perform the required traversal directly without materializing a reversed
|
|
38
|
+
kernel.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **Breaking:** Changed `convolve`, `convolve_basic`, and `convolve_fft` to
|
|
43
|
+
calculate mathematical discrete convolution. Version 2 users who need
|
|
44
|
+
unchanged results must rename these calls to their `correlate*` counterparts.
|
|
45
|
+
- **Breaking:** Changed the `predict_convolve_*_time` methods to estimate the
|
|
46
|
+
new mathematical convolution implementations.
|
|
47
|
+
- Defined operation-specific `:same` alignment. Correlation uses
|
|
48
|
+
`signal[i + j - anchor]`; convolution uses
|
|
49
|
+
`signal[i + anchor - j]`. This places the extra boundary sample for an even
|
|
50
|
+
kernel on opposite sides.
|
|
51
|
+
- Reworked linear PocketFFT calculations around real transforms and checked
|
|
52
|
+
exact/even or 2/3/5-smooth transform shapes. Circular calculations use real
|
|
53
|
+
transforms when an axis is even and retain complex transforms for all-odd
|
|
54
|
+
shapes.
|
|
55
|
+
|
|
56
|
+
### Removed
|
|
57
|
+
|
|
58
|
+
- **Breaking:** Removed the deprecated `convolve_fftw3` compatibility method.
|
|
59
|
+
Use `correlate_fft` to preserve version 2 results or `convolve_fft` for
|
|
60
|
+
mathematical convolution.
|
|
61
|
+
|
|
10
62
|
## [2.0.0] - 2026-08-20
|
|
11
63
|
|
|
12
64
|
### Added
|
|
@@ -81,7 +133,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
81
133
|
- Updated the native extension to use the maintained Numo C API and removed the
|
|
82
134
|
legacy untyped-data compatibility code.
|
|
83
135
|
|
|
84
|
-
[Unreleased]: https://github.com/neilslater/convolver/compare/
|
|
136
|
+
[Unreleased]: https://github.com/neilslater/convolver/compare/v3.0.0...HEAD
|
|
137
|
+
[3.0.0]: https://github.com/neilslater/convolver/compare/v2.0.0...v3.0.0
|
|
85
138
|
[2.0.0]: https://github.com/neilslater/convolver/compare/v1.0.1...v2.0.0
|
|
86
139
|
[1.0.1]: https://github.com/neilslater/convolver/compare/v1.0.0...v1.0.1
|
|
87
140
|
[1.0.0]: https://github.com/neilslater/convolver/compare/v0.3.2...v1.0.0
|
data/README.md
CHANGED
|
@@ -3,16 +3,14 @@
|
|
|
3
3
|
[](https://github.com/neilslater/convolver/actions/workflows/ci.yml)
|
|
4
4
|
[](https://badge.fury.io/rb/convolver)
|
|
5
5
|
|
|
6
|
-
Convolver calculates cross-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
Convolver calculates mathematical convolution and cross-correlation between
|
|
7
|
+
multidimensional
|
|
8
|
+
[`Numo::NArray`](https://github.com/yoshoku/numo-narray-alt) values. Both
|
|
9
|
+
operations support configurable output extents and signal boundary extensions.
|
|
10
|
+
Convolver chooses between a direct native implementation for smaller inputs
|
|
11
|
+
and a [`Numo::Pocketfft`](https://github.com/yoshoku/numo-pocketfft)-based
|
|
11
12
|
implementation for larger inputs.
|
|
12
13
|
|
|
13
|
-
Version 1.0 replaces the unmaintained `narray` and `fftw3` gems with
|
|
14
|
-
`numo-narray-alt` and `numo-pocketfft`.
|
|
15
|
-
|
|
16
14
|
## Installation
|
|
17
15
|
|
|
18
16
|
Add Convolver to your application's Gemfile:
|
|
@@ -21,22 +19,16 @@ Add Convolver to your application's Gemfile:
|
|
|
21
19
|
gem 'convolver'
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
Then run
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
bundle install
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Alternatively, install it directly:
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
gem install convolver
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
No external FFT library is required; PocketFFT is bundled by its Ruby gem.
|
|
22
|
+
Then run `bundle install`, or install the gem directly with
|
|
23
|
+
`gem install convolver`. No external FFT library is required; PocketFFT is
|
|
24
|
+
bundled by its Ruby gem.
|
|
37
25
|
|
|
38
26
|
## Usage
|
|
39
27
|
|
|
28
|
+
`convolve` calculates mathematical discrete convolution. `correlate`
|
|
29
|
+
calculates cross-correlation, following the public naming convention used by
|
|
30
|
+
NumPy and SciPy:
|
|
31
|
+
|
|
40
32
|
```ruby
|
|
41
33
|
require 'convolver'
|
|
42
34
|
|
|
@@ -45,32 +37,44 @@ kernel = Numo::SFloat[1.3, -0.5]
|
|
|
45
37
|
|
|
46
38
|
Convolver.convolve(signal, kernel)
|
|
47
39
|
# => Numo::SFloat#shape=[2]
|
|
40
|
+
# [0.37, 0.45]
|
|
41
|
+
|
|
42
|
+
Convolver.correlate(signal, kernel)
|
|
43
|
+
# => Numo::SFloat#shape=[2]
|
|
48
44
|
# [0.19, 0.27]
|
|
49
45
|
```
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
the signal and kernel must have the same rank, the kernel must be no larger
|
|
53
|
-
than the signal in any dimension, and only positions with complete overlap are
|
|
54
|
-
returned. The result size in each dimension is:
|
|
47
|
+
For real one-dimensional inputs, the two valid operations are:
|
|
55
48
|
|
|
56
|
-
```
|
|
57
|
-
|
|
49
|
+
```text
|
|
50
|
+
correlate(signal, kernel)[p] = sum_j signal[p + j] * conjugate(kernel[j])
|
|
51
|
+
convolve(signal, kernel)[n] = sum_j signal[n - j] * kernel[j]
|
|
58
52
|
```
|
|
59
53
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
The formulas apply component-wise to multidimensional arrays. Correlation
|
|
55
|
+
conventionally conjugates the second operand, although Convolver's current
|
|
56
|
+
single-precision real input contract makes conjugation invisible.
|
|
63
57
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
With no keywords, the signal and kernel must have the same rank, the kernel
|
|
59
|
+
must be no larger than the signal in any dimension, and only positions with
|
|
60
|
+
complete overlap are returned. Inputs are converted to single-precision floats
|
|
61
|
+
internally and results are returned as `Numo::SFloat`. Supplying
|
|
62
|
+
`Numo::SFloat` inputs avoids conversion in the direct implementation.
|
|
63
|
+
|
|
64
|
+
The automatic, direct, and FFT implementations are available for both
|
|
65
|
+
operations:
|
|
67
66
|
|
|
68
67
|
```ruby
|
|
68
|
+
Convolver.convolve(signal, kernel)
|
|
69
69
|
Convolver.convolve_basic(signal, kernel)
|
|
70
70
|
Convolver.convolve_fft(signal, kernel)
|
|
71
|
+
|
|
72
|
+
Convolver.correlate(signal, kernel)
|
|
73
|
+
Convolver.correlate_basic(signal, kernel)
|
|
74
|
+
Convolver.correlate_fft(signal, kernel)
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
Every calculation method accepts the same options:
|
|
74
78
|
|
|
75
79
|
```ruby
|
|
76
80
|
Convolver.convolve(signal, kernel,
|
|
@@ -113,45 +117,89 @@ passed with another boundary. `:reflect` repeats the edge sample; `:mirror`
|
|
|
113
117
|
does not. All boundary modes work across every dimension, including
|
|
114
118
|
length-one axes and extensions wider than the stored signal.
|
|
115
119
|
|
|
116
|
-
### Kernel origin
|
|
120
|
+
### Kernel origin and alignment
|
|
117
121
|
|
|
118
|
-
`origin:` shifts the kernel anchor for `:same`. It accepts one integer
|
|
119
|
-
to every dimension or an array with one integer per dimension. For a
|
|
120
|
-
dimension of length `K`:
|
|
122
|
+
`origin:` shifts the stored-kernel anchor for `:same`. It accepts one integer
|
|
123
|
+
applied to every dimension or an array with one integer per dimension. For a
|
|
124
|
+
kernel dimension of length `K`:
|
|
121
125
|
|
|
122
|
-
```
|
|
123
|
-
anchor = (K / 2) + origin
|
|
126
|
+
```text
|
|
127
|
+
anchor = floor(K / 2) + origin
|
|
128
|
+
|
|
129
|
+
correlation[i] = sum_j extended_signal[i + j - anchor] * kernel[j]
|
|
130
|
+
convolution[i] = sum_j extended_signal[i + anchor - j] * kernel[j]
|
|
124
131
|
```
|
|
125
132
|
|
|
126
|
-
The anchor must remain within the kernel.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
The anchor must remain within the kernel. Correlation pads `anchor` samples
|
|
134
|
+
before the signal and `K - 1 - anchor` after it; convolution swaps those
|
|
135
|
+
widths. Odd centered kernels therefore align alike. For an even centered
|
|
136
|
+
kernel, correlation puts the extra sample before the signal and convolution
|
|
137
|
+
puts it after the signal. Positive origins move correlation toward lower signal
|
|
138
|
+
indices and convolution toward higher signal indices.
|
|
139
|
+
|
|
140
|
+
Nonzero origins are supported only for `:same`; `:valid` and `:full` require
|
|
141
|
+
zero.
|
|
142
|
+
|
|
143
|
+
### Estimators
|
|
131
144
|
|
|
132
|
-
The estimator methods accept and validate the same options
|
|
145
|
+
The estimator methods accept and validate the same options as their calculation
|
|
146
|
+
families:
|
|
133
147
|
|
|
134
148
|
```ruby
|
|
135
149
|
Convolver.predict_convolve_basic_time(signal, kernel, mode: :same, boundary: :nearest)
|
|
136
150
|
Convolver.predict_convolve_fft_time(signal, kernel, mode: :same, boundary: :wrap)
|
|
151
|
+
|
|
152
|
+
Convolver.predict_correlate_basic_time(signal, kernel, mode: :same, boundary: :nearest)
|
|
153
|
+
Convolver.predict_correlate_fft_time(signal, kernel, mode: :same, boundary: :wrap)
|
|
137
154
|
```
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
Calculations and estimators reject unrepresentable buffer sizes with `RangeError`
|
|
157
|
+
before allocation. FFT planning also checks conservative PocketFFT integer and
|
|
158
|
+
work-buffer limits, including full complex staging for real inverse transforms.
|
|
159
|
+
Automatic selection uses direct calculation when FFT exceeds its limits and the
|
|
160
|
+
direct path remains valid. These checks do not reserve memory; ordinary allocator
|
|
161
|
+
exhaustion still raises `NoMemoryError`.
|
|
162
|
+
|
|
163
|
+
### Migrating from version 2
|
|
164
|
+
|
|
165
|
+
Version 2's `convolve*` methods calculated cross-correlation. Version 3 corrects
|
|
166
|
+
the terminology and changes every `convolve*` method to mathematical
|
|
167
|
+
convolution. Asymmetric kernels make the result change visible.
|
|
168
|
+
|
|
169
|
+
To preserve version 2 results, rename the complete method family:
|
|
170
|
+
|
|
171
|
+
| Version 2 | Version 3 equivalent |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `convolve` | `correlate` |
|
|
174
|
+
| `convolve_basic` | `correlate_basic` |
|
|
175
|
+
| `convolve_fft` | `correlate_fft` |
|
|
176
|
+
| `predict_convolve_basic_time` | `predict_correlate_basic_time` |
|
|
177
|
+
| `predict_convolve_fft_time` | `predict_correlate_fft_time` |
|
|
178
|
+
| `convolve_fftw3` | `correlate_fft` |
|
|
179
|
+
|
|
180
|
+
`convolve_fftw3` has been removed. No `cross_correlate` aliases are provided;
|
|
181
|
+
the standard `correlate` name denotes cross-correlation explicitly documented
|
|
182
|
+
above.
|
|
141
183
|
|
|
142
184
|
## Contributing
|
|
143
185
|
|
|
144
|
-
Install the development dependencies, then run the
|
|
186
|
+
Install the development dependencies, then run the complete local gate:
|
|
145
187
|
|
|
146
188
|
```sh
|
|
147
189
|
bundle install
|
|
148
190
|
bundle exec rake
|
|
149
191
|
bundle exec rubocop
|
|
192
|
+
bundle exec ncs-rubocop-conf-audit
|
|
150
193
|
bundle exec rake c:lint
|
|
194
|
+
bundle exec bundle-audit check --update
|
|
195
|
+
bundle exec rake docs:check
|
|
151
196
|
```
|
|
152
197
|
|
|
153
|
-
The Ruby specs exercise both the Ruby API and native extension
|
|
154
|
-
|
|
198
|
+
The Ruby specs exercise both the Ruby API and native extension and enforce 95%
|
|
199
|
+
line and branch coverage for the Ruby library. Dependency auditing refreshes
|
|
200
|
+
the advisory database and requires network access. The documentation check
|
|
201
|
+
rejects YARD warnings and undocumented public API objects; private implementation
|
|
202
|
+
classes are marked `@private`. Additional native-code checks are available:
|
|
155
203
|
|
|
156
204
|
```sh
|
|
157
205
|
bundle exec rake c:coverage # Requires GCC and gcovr
|
|
@@ -72,8 +72,6 @@ static inline size_t maximum_offset(
|
|
|
72
72
|
//
|
|
73
73
|
// Convolve
|
|
74
74
|
//
|
|
75
|
-
// Benchmark: 640x480 image, 8x8 kernel, 1000 iterations. 12.3 seconds.
|
|
76
|
-
//
|
|
77
75
|
|
|
78
76
|
void convolve_raw(
|
|
79
77
|
int in_rank, const size_t *in_shape, const float *in_ptr,
|
|
@@ -117,7 +115,7 @@ void convolve_raw(
|
|
|
117
115
|
offset = 0;
|
|
118
116
|
corner_reset( out_rank, out_shape, out_q );
|
|
119
117
|
|
|
120
|
-
// Main
|
|
118
|
+
// Main convolution loop
|
|
121
119
|
for ( i = 0; i < out_size; i++ ) {
|
|
122
120
|
#if CONVOLVER_USE_SSE
|
|
123
121
|
__m128 simd_x, simd_y, simd_t;
|
|
@@ -127,12 +125,16 @@ void convolve_raw(
|
|
|
127
125
|
float t = 0.0;
|
|
128
126
|
|
|
129
127
|
#if CONVOLVER_USE_SSE
|
|
130
|
-
//
|
|
128
|
+
// Contiguous kernel views need not start on a 16-byte boundary.
|
|
131
129
|
for ( j = 0; j < kernel_aligned; j +=4 ) {
|
|
132
|
-
simd_x =
|
|
130
|
+
simd_x = _mm_loadu_ps( kernel_ptr + j );
|
|
133
131
|
// Yes the backwards alignment is correct
|
|
134
|
-
simd_y = _mm_set_ps(
|
|
135
|
-
|
|
132
|
+
simd_y = _mm_set_ps(
|
|
133
|
+
in_ptr[ offset + kernel_co_incr_cache[kernel_size - 1 - (j+3)] ],
|
|
134
|
+
in_ptr[ offset + kernel_co_incr_cache[kernel_size - 1 - (j+2)] ],
|
|
135
|
+
in_ptr[ offset + kernel_co_incr_cache[kernel_size - 1 - (j+1)] ],
|
|
136
|
+
in_ptr[ offset + kernel_co_incr_cache[kernel_size - 1 - j] ]
|
|
137
|
+
);
|
|
136
138
|
simd_x = _mm_mul_ps( simd_x, simd_y );
|
|
137
139
|
simd_t = _mm_add_ps( simd_x, simd_t );
|
|
138
140
|
}
|
|
@@ -141,13 +143,13 @@ void convolve_raw(
|
|
|
141
143
|
// SSE is unavailable on architectures such as Apple Silicon.
|
|
142
144
|
// Use the same calculation without x86-specific intrinsics.
|
|
143
145
|
for ( j = 0; j < kernel_aligned; j++ ) {
|
|
144
|
-
t += in_ptr[ offset + kernel_co_incr_cache[j] ] * kernel_ptr[j];
|
|
146
|
+
t += in_ptr[ offset + kernel_co_incr_cache[kernel_size - 1 - j] ] * kernel_ptr[j];
|
|
145
147
|
}
|
|
146
148
|
#endif
|
|
147
149
|
|
|
148
150
|
// Complete any remaining 1,2 or 3 items one at a time
|
|
149
151
|
for ( j = kernel_aligned; j < kernel_size; j++ ) {
|
|
150
|
-
t += in_ptr[ offset + kernel_co_incr_cache[j] ] * kernel_ptr[ j ];
|
|
152
|
+
t += in_ptr[ offset + kernel_co_incr_cache[kernel_size - 1 - j] ] * kernel_ptr[ j ];
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
#if CONVOLVER_USE_SSE
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
#ifndef CONVOLVE_RAW_H
|
|
9
9
|
#define CONVOLVE_RAW_H
|
|
10
10
|
|
|
11
|
+
#ifndef CONVOLVER_RAW_CONFIG
|
|
12
|
+
#define CONVOLVER_RAW_CONFIG
|
|
11
13
|
#include <ruby.h>
|
|
12
14
|
#if defined(__SSE__)
|
|
13
15
|
#include <xmmintrin.h>
|
|
@@ -16,6 +18,7 @@
|
|
|
16
18
|
#define CONVOLVER_USE_SSE 0
|
|
17
19
|
#endif
|
|
18
20
|
#define LARGEST_RANK 16
|
|
21
|
+
#endif
|
|
19
22
|
|
|
20
23
|
void convolve_raw(
|
|
21
24
|
int in_rank, const size_t *in_shape, const float *in_ptr,
|
data/ext/convolver/convolver.c
CHANGED
|
@@ -2,10 +2,33 @@
|
|
|
2
2
|
#include <numo/narray.h>
|
|
3
3
|
#include <numo/intern.h>
|
|
4
4
|
|
|
5
|
+
#include "correlate_raw.h"
|
|
5
6
|
#include "convolve_raw.h"
|
|
6
7
|
|
|
7
8
|
static VALUE mConvolver;
|
|
8
9
|
|
|
10
|
+
/* Contiguous views share their backing storage but start at a byte offset. */
|
|
11
|
+
static const float *logical_pointer_for_read(VALUE value) {
|
|
12
|
+
return (const float *)(na_get_pointer_for_read(value) + na_get_offset(value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static VALUE contiguous_sfloat(VALUE value) {
|
|
16
|
+
narray_t *narray;
|
|
17
|
+
|
|
18
|
+
value = rb_funcall(numo_cSFloat, rb_intern("cast"), 1, value);
|
|
19
|
+
GetNArray(value, narray);
|
|
20
|
+
/* A scalar has no strides; Numo's contiguous check requires an axis. */
|
|
21
|
+
if (narray->ndim > 0 && !RTEST(na_check_contiguous(value))) {
|
|
22
|
+
value = na_copy(value);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
enum convolver_operation {
|
|
28
|
+
CONVOLVER_CORRELATION,
|
|
29
|
+
CONVOLVER_CONVOLUTION
|
|
30
|
+
};
|
|
31
|
+
|
|
9
32
|
static void copy_shape(int rank, const size_t *source, size_t *target) {
|
|
10
33
|
int i;
|
|
11
34
|
|
|
@@ -14,15 +37,7 @@ static void copy_shape(int rank, const size_t *source, size_t *target) {
|
|
|
14
37
|
}
|
|
15
38
|
}
|
|
16
39
|
|
|
17
|
-
|
|
18
|
-
* Calculates a valid cross-correlation using the direct native implementation.
|
|
19
|
-
*
|
|
20
|
-
* @overload convolve_basic_valid(signal, kernel)
|
|
21
|
-
* @param signal [Numo::NArray] input values
|
|
22
|
-
* @param kernel [Numo::NArray] correlation kernel
|
|
23
|
-
* @return [Numo::SFloat] valid cross-correlation result
|
|
24
|
-
*/
|
|
25
|
-
static VALUE convolver_convolve_basic_valid(VALUE self, VALUE signal, VALUE kernel) {
|
|
40
|
+
static VALUE convolver_basic_valid(VALUE signal, VALUE kernel, enum convolver_operation operation) {
|
|
26
41
|
volatile VALUE signal_value;
|
|
27
42
|
volatile VALUE kernel_value;
|
|
28
43
|
volatile VALUE result_value;
|
|
@@ -35,20 +50,12 @@ static VALUE convolver_convolve_basic_valid(VALUE self, VALUE signal, VALUE kern
|
|
|
35
50
|
size_t result_shape[LARGEST_RANK];
|
|
36
51
|
size_t numo_result_shape[LARGEST_RANK];
|
|
37
52
|
|
|
38
|
-
(void)self;
|
|
39
|
-
|
|
40
53
|
if (!rb_obj_is_kind_of(signal, numo_cNArray) || !rb_obj_is_kind_of(kernel, numo_cNArray)) {
|
|
41
54
|
rb_raise(rb_eArgError, "signal and kernel must be Numo::NArray values");
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
signal_value =
|
|
45
|
-
kernel_value =
|
|
46
|
-
if (!RTEST(na_check_contiguous(signal_value))) {
|
|
47
|
-
signal_value = na_copy(signal_value);
|
|
48
|
-
}
|
|
49
|
-
if (!RTEST(na_check_contiguous(kernel_value))) {
|
|
50
|
-
kernel_value = na_copy(kernel_value);
|
|
51
|
-
}
|
|
57
|
+
signal_value = contiguous_sfloat(signal);
|
|
58
|
+
kernel_value = contiguous_sfloat(kernel);
|
|
52
59
|
GetNArray(signal_value, signal_narray);
|
|
53
60
|
GetNArray(kernel_value, kernel_narray);
|
|
54
61
|
|
|
@@ -76,16 +83,53 @@ static VALUE convolver_convolve_basic_valid(VALUE self, VALUE signal, VALUE kern
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
result_value = nary_new(numo_cSFloat, rank, numo_result_shape);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
if (operation == CONVOLVER_CORRELATION) {
|
|
87
|
+
correlate_raw(
|
|
88
|
+
rank, signal_shape, logical_pointer_for_read(signal_value),
|
|
89
|
+
rank, kernel_shape, logical_pointer_for_read(kernel_value),
|
|
90
|
+
rank, result_shape, (float *)na_get_pointer_for_write(result_value)
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
convolve_raw(
|
|
94
|
+
rank, signal_shape, logical_pointer_for_read(signal_value),
|
|
95
|
+
rank, kernel_shape, logical_pointer_for_read(kernel_value),
|
|
96
|
+
rank, result_shape, (float *)na_get_pointer_for_write(result_value)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
84
99
|
|
|
85
100
|
return result_value;
|
|
86
101
|
}
|
|
87
102
|
|
|
103
|
+
/*
|
|
104
|
+
* Calculates a valid cross-correlation using the direct native implementation.
|
|
105
|
+
*
|
|
106
|
+
* @private
|
|
107
|
+
* @overload correlate_basic_valid(signal, kernel)
|
|
108
|
+
* @param signal [Numo::NArray] input values
|
|
109
|
+
* @param kernel [Numo::NArray] correlation kernel
|
|
110
|
+
* @return [Numo::SFloat] valid cross-correlation result
|
|
111
|
+
*/
|
|
112
|
+
static VALUE convolver_correlate_basic_valid(VALUE self, VALUE signal, VALUE kernel) {
|
|
113
|
+
(void)self;
|
|
114
|
+
return convolver_basic_valid(signal, kernel, CONVOLVER_CORRELATION);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* Calculates a valid mathematical convolution using the direct native implementation.
|
|
119
|
+
*
|
|
120
|
+
* @private
|
|
121
|
+
* @overload convolve_basic_valid(signal, kernel)
|
|
122
|
+
* @param signal [Numo::NArray] input values
|
|
123
|
+
* @param kernel [Numo::NArray] convolution kernel
|
|
124
|
+
* @return [Numo::SFloat] valid mathematical convolution result
|
|
125
|
+
*/
|
|
126
|
+
static VALUE convolver_convolve_basic_valid(VALUE self, VALUE signal, VALUE kernel) {
|
|
127
|
+
(void)self;
|
|
128
|
+
return convolver_basic_valid(signal, kernel, CONVOLVER_CONVOLUTION);
|
|
129
|
+
}
|
|
130
|
+
|
|
88
131
|
void Init_convolver(void) {
|
|
89
132
|
mConvolver = rb_define_module("Convolver");
|
|
133
|
+
rb_define_singleton_method(mConvolver, "correlate_basic_valid", convolver_correlate_basic_valid, 2);
|
|
90
134
|
rb_define_singleton_method(mConvolver, "convolve_basic_valid", convolver_convolve_basic_valid, 2);
|
|
91
135
|
}
|
|
@@ -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
|
+
}
|