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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08959da24c94d02889f0067f18f7233578b4b14ec4e58538b144efeef1cbe25f'
4
- data.tar.gz: c39df2ebf01a04f93fa62e49bd8a4542e78244868d822d7f47a00cc78b9aaa7e
3
+ metadata.gz: e56b4c0d87d92abb3f8c79d3d11b8d41c8dcbb6d7055dce615eef07a363b0cc2
4
+ data.tar.gz: 23c852764b162f082fa380e6932a8c5ad0c400c6d766579d7a087d32cd875825
5
5
  SHA512:
6
- metadata.gz: 73d425b7851b21dd93cb135ca7ad88aa75f760b41a53ed8a2b72b2a9110e183062c1643af9563f81d59248d50d3f49a531e2bbac6d0c5cdcef6383717e31f73a
7
- data.tar.gz: 89ddc9f26950329b9ce2ce3917e5f90057c3c625f2210eb39fcc527cbe7b3f418f14f387665a3be3c1ea44d50beac101907c038c6ef9998998e30c21d61966d2
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/v2.0.0...HEAD
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
  [![CI](https://github.com/neilslater/convolver/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/neilslater/convolver/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/convolver.svg)](https://badge.fury.io/rb/convolver)
5
5
 
6
- Convolver calculates cross-correlations between multidimensional
7
- [`Numo::NArray`](https://github.com/yoshoku/numo-narray-alt) values, with
8
- configurable output extents and signal boundary extensions. It chooses between
9
- a direct native implementation for smaller inputs and a
10
- [`Numo::Pocketfft`](https://github.com/yoshoku/numo-pocketfft)-based
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
- ```sh
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
- With no keywords, Convolver preserves its original valid-correlation behavior:
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
- ```ruby
57
- signal_size - kernel_size + 1
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
- Inputs are converted to single-precision floats internally, and results are
61
- returned as `Numo::SFloat`. Supplying `Numo::SFloat` inputs avoids conversion in
62
- the direct implementation.
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
- `Convolver.convolve` normally chooses the implementation automatically. The
65
- implementations can also be called directly for application-specific
66
- benchmarking:
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
- Both implementations and the automatic method accept the same options:
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 applied
119
- to every dimension or an array with one integer per dimension. For a kernel
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
- ```ruby
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. Positive origins sample farther
127
- toward lower signal indices. With the default origin, an even kernel gives the
128
- extra boundary sample to the lower-index, or left, side: a length-four kernel
129
- uses two samples before and one after the aligned signal position. Nonzero
130
- origins are supported only for `:same`; `:valid` and `:full` require zero.
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
- `Convolver.convolve_fftw3` remains as a deprecated alias for `convolve_fft` to
140
- ease migration from Convolver 0.x and forwards all options.
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 main suite:
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. Additional
154
- native-code checks are available:
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 convolve loop
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
- // Use SIMD for all the aligned values in groups of 4
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 = _mm_load_ps( kernel_ptr + j );
130
+ simd_x = _mm_loadu_ps( kernel_ptr + j );
133
131
  // Yes the backwards alignment is correct
134
- simd_y = _mm_set_ps( in_ptr[ offset + kernel_co_incr_cache[j+3] ], in_ptr[ offset + kernel_co_incr_cache[j+2] ],
135
- in_ptr[ offset + kernel_co_incr_cache[j+1] ], in_ptr[ offset + kernel_co_incr_cache[j] ] );
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,
@@ -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 = rb_funcall(numo_cSFloat, rb_intern("cast"), 1, signal);
45
- kernel_value = rb_funcall(numo_cSFloat, rb_intern("cast"), 1, kernel);
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
- convolve_raw(
80
- rank, signal_shape, (float *)na_get_pointer_for_read(signal_value),
81
- rank, kernel_shape, (float *)na_get_pointer_for_read(kernel_value),
82
- rank, result_shape, (float *)na_get_pointer_for_write(result_value)
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
+ }