convolver 3.0.1 → 4.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.
@@ -0,0 +1,116 @@
1
+ #include "fft_shape.h"
2
+ #include "raw_config.h"
3
+ #include <limits.h>
4
+ #include <math.h>
5
+
6
+ static size_t positive_size(VALUE value) {
7
+ if (!RB_INTEGER_TYPE_P(value) || RTEST(rb_funcall(value, rb_intern("<="), 1, INT2FIX(0)))) {
8
+ rb_raise(rb_eArgError, "FFT dimensions and size limit must be positive integers");
9
+ }
10
+ return NUM2SIZET(value);
11
+ }
12
+
13
+ static long read_shape(VALUE shape, size_t *dimensions) {
14
+ Check_Type(shape, T_ARRAY);
15
+ long rank = RARRAY_LEN(shape);
16
+ if (rank < 1 || rank > LARGEST_RANK) {
17
+ rb_raise(rb_eArgError, "FFT shape rank must be between 1 and %d", LARGEST_RANK);
18
+ }
19
+ for (long axis = 0; axis < rank; axis++) {
20
+ dimensions[axis] = positive_size(rb_ary_entry(shape, axis));
21
+ }
22
+ return rank;
23
+ }
24
+
25
+ static size_t doubled_candidate(size_t initial, size_t target, int even, size_t maximum) {
26
+ size_t candidate = initial;
27
+ while (candidate < target || (even && candidate % 2 != 0)) {
28
+ if (candidate > maximum / 2) return 0;
29
+ candidate *= 2;
30
+ }
31
+ return candidate;
32
+ }
33
+
34
+ /* Enumerate 3/5 powers only while they can improve the best 2/3/5 candidate. */
35
+ static size_t next_fast_size(size_t target, int even, size_t maximum) {
36
+ size_t best = 0, five = 1;
37
+ for (;;) {
38
+ size_t three = five;
39
+ for (;;) {
40
+ size_t candidate = doubled_candidate(three, target, even, maximum);
41
+ if (candidate != 0 && (best == 0 || candidate < best)) best = candidate;
42
+ size_t bound = best != 0 ? best : maximum;
43
+ if (three > bound / 3) break;
44
+ three *= 3;
45
+ }
46
+ size_t bound = best != 0 ? best : maximum;
47
+ if (five > bound / 5) break;
48
+ five *= 5;
49
+ }
50
+ return best;
51
+ }
52
+
53
+ /*
54
+ * Generates optional smooth dimensions without allocating numeric buffers.
55
+ * @private
56
+ */
57
+ static VALUE convolver_fft_fast_shape(VALUE self, VALUE shape, VALUE maximum_value) {
58
+ (void)self;
59
+ size_t dimensions[LARGEST_RANK];
60
+ long rank = read_shape(shape, dimensions);
61
+ size_t maximum = positive_size(maximum_value);
62
+ for (long axis = 0; axis < rank; axis++) {
63
+ if (dimensions[axis] > maximum) return Qnil;
64
+ dimensions[axis] = next_fast_size(dimensions[axis], axis == rank - 1, maximum);
65
+ if (dimensions[axis] == 0) return Qnil;
66
+ }
67
+ VALUE result = rb_ary_new_capa(rank);
68
+ for (long axis = 0; axis < rank; axis++) {
69
+ rb_ary_push(result, SIZET2NUM(dimensions[axis]));
70
+ }
71
+ return result;
72
+ }
73
+
74
+ static double large_factor_penalty(size_t value) {
75
+ double penalty = 0.0;
76
+ size_t divisor = 2;
77
+ while (divisor <= value / divisor) {
78
+ while (value % divisor == 0) {
79
+ if (divisor > 7) penalty += 1.5 * (log2((double)divisor) - 3.0);
80
+ value /= divisor;
81
+ }
82
+ divisor += divisor == 2 ? 1 : 2;
83
+ }
84
+ if (value > 7) penalty += 1.5 * (log2((double)value) - 3.0);
85
+ return penalty;
86
+ }
87
+
88
+ /*
89
+ * Scores an admissible candidate; Ruby owns the complete FFT buffer policy.
90
+ * @private
91
+ */
92
+ static VALUE convolver_fft_shape_cost(VALUE self, VALUE shape) {
93
+ (void)self;
94
+ size_t dimensions[LARGEST_RANK], product = 1;
95
+ long rank = read_shape(shape, dimensions);
96
+ for (long axis = 0; axis < rank; axis++) {
97
+ /* Bound trial factorization even if this private primitive is called directly. */
98
+ if (dimensions[axis] > (size_t)INT_MAX / 16) {
99
+ rb_raise(rb_eRangeError, "FFT scoring axis exceeds native integer limit");
100
+ }
101
+ if (dimensions[axis] > SIZE_MAX / product) {
102
+ rb_raise(rb_eRangeError, "FFT scoring size exceeds native element limit");
103
+ }
104
+ product *= dimensions[axis];
105
+ }
106
+ double cost = 0.0;
107
+ for (long axis = 0; axis < rank; axis++) {
108
+ cost += log2((double)dimensions[axis]) + large_factor_penalty(dimensions[axis]);
109
+ }
110
+ return DBL2NUM((double)product * cost);
111
+ }
112
+
113
+ void convolver_init_fft_shape(VALUE convolver) {
114
+ rb_define_singleton_method(convolver, "fft_fast_shape", convolver_fft_fast_shape, 2);
115
+ rb_define_singleton_method(convolver, "fft_shape_cost", convolver_fft_shape_cost, 1);
116
+ }
@@ -0,0 +1,8 @@
1
+ #ifndef CONVOLVER_FFT_SHAPE_H
2
+ #define CONVOLVER_FFT_SHAPE_H
3
+
4
+ #include <ruby.h>
5
+
6
+ void convolver_init_fft_shape(VALUE convolver);
7
+
8
+ #endif
@@ -0,0 +1,11 @@
1
+ #ifndef CONVOLVER_RAW_CONFIG_H
2
+ #define CONVOLVER_RAW_CONFIG_H
3
+ #include <ruby.h>
4
+ #if defined(__SSE__)
5
+ #include <xmmintrin.h>
6
+ #endif
7
+ #if defined(__SSE2__)
8
+ #include <emmintrin.h>
9
+ #endif
10
+ #define LARGEST_RANK 16
11
+ #endif
@@ -0,0 +1,71 @@
1
+ #ifndef CONVOLVER_RAW_HELPERS_H
2
+ #define CONVOLVER_RAW_HELPERS_H
3
+
4
+ static inline size_t checked_add( size_t left, size_t right, const char *description ) {
5
+ if ( right > SIZE_MAX - left ) {
6
+ rb_raise( rb_eRangeError, "%s exceeds native implementation limit", description );
7
+ }
8
+ return left + right;
9
+ }
10
+
11
+ static inline size_t checked_multiply( size_t left, size_t right, const char *description ) {
12
+ if ( left != 0 && right > SIZE_MAX / left ) {
13
+ rb_raise( rb_eRangeError, "%s exceeds native implementation limit", description );
14
+ }
15
+ return left * right;
16
+ }
17
+
18
+ static inline size_t size_from_shape( int rank, const size_t *shape, const char *description ) {
19
+ size_t size = 1;
20
+ int i;
21
+ for ( i = 0; i < rank; i++ ) {
22
+ size = checked_multiply( size, shape[i], description );
23
+ }
24
+ return size;
25
+ }
26
+
27
+ // Sets reverse indices
28
+ static inline void corner_reset( int rank, const size_t *shape, size_t *rev_indices ) {
29
+ int i;
30
+ for ( i = 0; i < rank; i++ ) { rev_indices[i] = shape[i] - 1; }
31
+ }
32
+
33
+ // Counts indices down, returns number of ranks that reset
34
+ static inline int corner_dec( int rank, const size_t *shape, size_t *rev_indices ) {
35
+ int i = 0;
36
+ (void) rank;
37
+ while ( ! rev_indices[i]-- ) {
38
+ rev_indices[i] = shape[i] - 1;
39
+ i++;
40
+ }
41
+ return i;
42
+ }
43
+
44
+ // Generates co-increment steps by rank boundaries crossed, for the outer position as inner position is incremented by 1
45
+ static inline void calc_co_increment(
46
+ int rank, const size_t *outer_shape, const size_t *inner_shape, size_t *co_increment ) {
47
+ size_t factor = 1;
48
+ int i;
49
+ co_increment[0] = 1; // co-increment is always 1 in lowest rank
50
+ for ( i = 0; i < rank; i++ ) {
51
+ size_t skipped = checked_multiply( factor, outer_shape[i] - inner_shape[i], "array offset" );
52
+ co_increment[i+1] = checked_add( co_increment[i], skipped, "array offset" );
53
+ factor = checked_multiply( factor, outer_shape[i], "array size" );
54
+ }
55
+ }
56
+
57
+ static inline size_t maximum_offset(
58
+ int rank, const size_t *outer_shape, const size_t *inner_shape, const char *description ) {
59
+ size_t factor = 1;
60
+ size_t offset = 0;
61
+ int i;
62
+ for ( i = 0; i < rank; i++ ) {
63
+ size_t dimension_offset = checked_multiply( factor, inner_shape[i] - 1, description );
64
+ offset = checked_add( offset, dimension_offset, description );
65
+ factor = checked_multiply( factor, outer_shape[i], "array size" );
66
+ }
67
+ return offset;
68
+ }
69
+
70
+
71
+ #endif
@@ -0,0 +1,93 @@
1
+ /* Intentionally included once per dtype; operation/dtype dispatch is outside hot loops. */
2
+ void RAW_NAME(
3
+ int in_rank, const size_t *in_shape, const RAW_TYPE *in_ptr,
4
+ int kernel_rank, const size_t *kernel_shape, const RAW_TYPE *kernel_ptr,
5
+ int out_rank, const size_t *out_shape, RAW_TYPE *out_ptr ) {
6
+ size_t i, j, input_size, kernel_size, kernel_aligned, out_size, offset;
7
+ size_t maximum_input_offset;
8
+ size_t out_co_incr[LARGEST_RANK + 1], kernel_co_incr[LARGEST_RANK + 1];
9
+ size_t ker_q[LARGEST_RANK], out_q[LARGEST_RANK];
10
+ size_t *kernel_co_incr_cache;
11
+ VALUE cache_storage = 0;
12
+
13
+ kernel_size = size_from_shape( kernel_rank, kernel_shape, "kernel size" );
14
+ kernel_aligned = kernel_size - kernel_size % RAW_LANES;
15
+ out_size = size_from_shape( out_rank, out_shape, "output size" );
16
+ input_size = size_from_shape( in_rank, in_shape, "input size" );
17
+
18
+ calc_co_increment( in_rank, in_shape, out_shape, out_co_incr );
19
+ calc_co_increment( in_rank, in_shape, kernel_shape, kernel_co_incr );
20
+ maximum_input_offset = checked_add(
21
+ maximum_offset( in_rank, in_shape, out_shape, "input offset" ),
22
+ maximum_offset( in_rank, in_shape, kernel_shape, "input offset" ),
23
+ "input offset"
24
+ );
25
+ if ( maximum_input_offset >= input_size ) {
26
+ rb_raise( rb_eRangeError, "input offset exceeds native implementation limit" );
27
+ }
28
+
29
+ kernel_co_incr_cache = RB_ALLOCV_N( size_t, cache_storage, kernel_size );
30
+ kernel_co_incr_cache[0] = 0;
31
+
32
+ corner_reset( kernel_rank, kernel_shape, ker_q );
33
+ for ( i = 1; i < kernel_size; i++ ) {
34
+ kernel_co_incr_cache[i] = checked_add(
35
+ kernel_co_incr_cache[i-1],
36
+ kernel_co_incr[ corner_dec( kernel_rank, kernel_shape, ker_q ) ],
37
+ "kernel offset"
38
+ );
39
+ }
40
+
41
+ offset = 0;
42
+ corner_reset( out_rank, out_shape, out_q );
43
+
44
+ for ( i = 0; i < out_size; i++ ) {
45
+ RAW_TYPE t = 0;
46
+ #if RAW_SIMD
47
+ RAW_VECTOR simd_t = RAW_ZERO();
48
+ RAW_TYPE v[RAW_LANES];
49
+ for ( j = 0; j < kernel_aligned; j += RAW_LANES ) {
50
+ RAW_VECTOR simd_x = RAW_LOAD(kernel_ptr + j);
51
+ #if RAW_LANES == 4
52
+ RAW_VECTOR simd_y = _mm_set_ps(
53
+ in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j+3)]],
54
+ in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j+2)]],
55
+ in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j+1)]],
56
+ in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j)]]);
57
+ #else
58
+ RAW_VECTOR simd_y = _mm_set_pd(
59
+ in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j+1)]],
60
+ in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j)]]);
61
+ #endif
62
+ simd_t = RAW_ADD(RAW_MUL(simd_x, simd_y), simd_t);
63
+ }
64
+ RAW_STORE(v, simd_t);
65
+ #else
66
+ for ( j = 0; j < kernel_aligned; j++ ) {
67
+ t += in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j)]] * kernel_ptr[j];
68
+ }
69
+ #endif
70
+ for ( j = kernel_aligned; j < kernel_size; j++ ) {
71
+ t += in_ptr[offset + kernel_co_incr_cache[RAW_INDEX(j)]] * kernel_ptr[j];
72
+ }
73
+ #if RAW_SIMD
74
+ #if RAW_LANES == 4
75
+ out_ptr[i] = v[0] + v[1] + v[2] + v[3] + t;
76
+ #else
77
+ out_ptr[i] = v[0] + v[1] + t;
78
+ #endif
79
+ #else
80
+ out_ptr[i] = t;
81
+ #endif
82
+
83
+ if ( i + 1 < out_size ) {
84
+ offset = checked_add(
85
+ offset,
86
+ out_co_incr[ corner_dec( out_rank, out_shape, out_q ) ],
87
+ "input offset"
88
+ );
89
+ }
90
+ }
91
+
92
+ RB_ALLOCV_END( cache_storage );
93
+ }
@@ -35,15 +35,15 @@ module Convolver
35
35
  attr_reader :plan, :signal_size, :kernel_size, :limits
36
36
 
37
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')
38
+ limits.buffer!(plan.result_size, plan.dtype::ELEMENT_BYTE_SIZE, 'result buffer')
39
+ limits.buffer!(signal_size, plan.dtype::ELEMENT_BYTE_SIZE, 'signal conversion buffer')
40
+ limits.buffer!(kernel_size, plan.dtype::ELEMENT_BYTE_SIZE, 'kernel conversion buffer')
41
41
  end
42
42
 
43
43
  def extension!
44
44
  return if plan.valid?
45
45
 
46
- limits.buffer!(plan.extended_size, 4, 'extended signal buffer')
46
+ limits.buffer!(plan.extended_size, plan.dtype::ELEMENT_BYTE_SIZE, 'extended signal buffer')
47
47
  return if plan.boundary == :constant
48
48
 
49
49
  plan.extended_shape.each do |length|
@@ -5,6 +5,7 @@ module Convolver
5
5
  # @private
6
6
  class CircularFftOperation
7
7
  def initialize(operation, signal, kernel, plan)
8
+ @dtype = plan.dtype
8
9
  @real_axis = plan.fft_buffers.real_axis
9
10
  @operation = operation
10
11
  @signal = Numo::DFloat.cast(signal)
@@ -28,7 +29,7 @@ module Convolver
28
29
 
29
30
  private
30
31
 
31
- attr_reader :operation, :signal, :kernel, :real_axis
32
+ attr_reader :operation, :signal, :kernel, :real_axis, :dtype
32
33
 
33
34
  def real_product(real_axis)
34
35
  final_axis = signal.ndim - 1
@@ -36,7 +37,7 @@ module Convolver
36
37
  prepared_kernel = prepare_real_operand(kernel, real_axis, final_axis)
37
38
  result = real_spectrum_product(prepared_signal, prepared_kernel)
38
39
  result = result.swapaxes(real_axis, final_axis) unless real_axis == final_axis
39
- Numo::SFloat.cast(result)
40
+ dtype.cast(result)
40
41
  end
41
42
 
42
43
  def prepare_real_operand(value, real_axis, final_axis)
@@ -55,7 +56,7 @@ module Convolver
55
56
  kernel_spectrum = Numo::Pocketfft.fftn(kernel)
56
57
  kernel_spectrum.inplace.conj if operation == :correlation
57
58
  result = Numo::Pocketfft.ifftn(signal_spectrum.inplace * kernel_spectrum).real
58
- Numo::SFloat.cast(result)
59
+ dtype.cast(result)
59
60
  end
60
61
 
61
62
  def fold_kernel(source, signal_shape, anchors)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Convolver
4
+ # Immutable measured-environment coefficients in seconds; ranks one through three.
5
+ # @private
6
+ module CostProfiles
7
+ MAC = {
8
+ direct: [[6.55e-6, 3.24e-10, 7.08e-10].freeze, [6.55e-6, 3.24e-10, 7.08e-10].freeze].freeze,
9
+ fixed: [1.64e-5, 5.12e-5, 1.13e-4].freeze,
10
+ transform: [8.02e-10, 7.17e-10, 1.54e-9].freeze,
11
+ indices: { reflect: 1.33e-7, nearest: 1.46e-7, mirror: 1.57e-7, wrap: 3.23e-8 }.freeze,
12
+ folding: [3.78e-7, 3.99e-7, 5.09e-7].freeze
13
+ }.freeze
14
+ X86 = {
15
+ direct: [[1.59e-5, 3.08e-10, 2.52e-10].freeze, [1.63e-5, 3.22e-10, 3.96e-10].freeze].freeze,
16
+ fixed: [3.77e-5, 1.20e-4, 1.98e-4].freeze,
17
+ transform: [2.00e-9, 2.13e-9, 3.13e-9].freeze,
18
+ indices: { reflect: 2.32e-7, nearest: 2.88e-7, mirror: 2.62e-7, wrap: 1.11e-7 }.freeze,
19
+ folding: [8.20e-7, 7.51e-7, 9.79e-7].freeze
20
+ }.freeze
21
+ ARM = {
22
+ direct: [[1.88e-5, 5.12e-10, 5.96e-10].freeze, [1.88e-5, 5.12e-10, 5.96e-10].freeze].freeze,
23
+ fixed: [4.20e-5, 1.20e-4, 2.70e-4].freeze,
24
+ transform: [3.29e-9, 2.89e-9, 3.74e-9].freeze,
25
+ indices: { reflect: 2.62e-7, nearest: 3.67e-7, mirror: 2.87e-7, wrap: 1.50e-7 }.freeze,
26
+ folding: [9.97e-7, 8.80e-7, 1.24e-6].freeze
27
+ }.freeze
28
+ def self.for_platform(platform)
29
+ case platform
30
+ when /\Aarm64-darwin/ then MAC
31
+ when /\Ax86_64-linux/ then X86
32
+ when /\Aaarch64-linux/ then ARM
33
+ end
34
+ end
35
+
36
+ CURRENT = for_platform(RUBY_PLATFORM)
37
+
38
+ def self.for_rank(rank)
39
+ CURRENT if (1..3).cover?(rank)
40
+ end
41
+ end
42
+
43
+ private_constant :CostProfiles
44
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'convolver/preparation_cost'
4
+
5
+ module Convolver
6
+ # Shared transform costs and a lower bound that never constructs an FFT plan.
7
+ # @private
8
+ class FftCost
9
+ REAL = { 1 => 1.0e-9, 2 => 1.0e-9, 3 => 1.5e-9 }.freeze
10
+ COMPLEX = { 1 => 2.4e-9, 2 => 2.4e-9, 3 => 2.8e-9 }.freeze
11
+ FIXED = { 0 => 5.0e-6, 1 => 1.5e-5, 2 => 3.0e-5, 3 => 1.0e-4 }.freeze
12
+ PREPARATION = 1.0e-9
13
+
14
+ def self.fixed(rank, profile: nil)
15
+ return profile.fetch(:fixed).fetch(rank - 1) if profile
16
+
17
+ FIXED.fetch(rank, FIXED.values.last)
18
+ end
19
+
20
+ def self.transform(size, rank, real: true, profile: nil)
21
+ coefficient(rank, real:, profile:) * size * Math.log([size, 2].max)
22
+ end
23
+
24
+ def self.lower_bound(plan, kernel_shape, threshold:)
25
+ rank = kernel_shape.length
26
+ profile = plan.cost_profile
27
+ setup = fixed(rank, profile:)
28
+ return setup if setup >= threshold || plan.wrap? || rank.zero?
29
+
30
+ size = minimum_size(plan.extended_shape, kernel_shape)
31
+ setup + transform(size, rank, profile:) + (2 * PREPARATION * size) + PreparationCost.new(plan).indices
32
+ end
33
+
34
+ def self.coefficient(rank, real:, profile:)
35
+ return profile.fetch(:transform).fetch(rank - 1) * (real ? 1 : 2.2) if profile
36
+
37
+ costs = real ? REAL : COMPLEX
38
+ costs.fetch(rank, costs.values.last)
39
+ end
40
+ private_class_method :coefficient
41
+
42
+ def self.minimum_size(signal_shape, kernel_shape)
43
+ # Ruby integers avoid overflow: this is only a cost bound, never an allocation.
44
+ shape = signal_shape.zip(kernel_shape).map { |signal, kernel| signal + kernel - 1 }
45
+ shape[-1] += 1 if shape[-1].odd?
46
+ shape.reduce(1, :*)
47
+ end
48
+ private_class_method :minimum_size
49
+ end
50
+
51
+ private_constant :FftCost
52
+ end
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'convolver/fft_cost'
4
+
3
5
  module Convolver
4
6
  # Estimates the selected real or complex PocketFFT implementation cost.
5
7
  # @private
6
8
  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
9
  AXIS_MOVE_COST = 1.6e-9
10
+ RESULT_COST = 5.0e-10
12
11
  CORRELATION_COST = 2.0e-10
13
12
 
14
13
  def initialize(operation, signal, kernel, plan)
@@ -16,54 +15,48 @@ module Convolver
16
15
  @signal = signal
17
16
  @kernel = kernel
18
17
  @plan = plan
18
+ @profile = plan.cost_profile
19
19
  @buffers = plan.fft_buffers
20
+ @preparation = PreparationCost.new(plan)
20
21
  end
21
22
 
22
23
  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
24
+ return FftCost.fixed(0) + preparation.conversion + result_cost if signal.ndim.zero?
25
+
26
+ transform_cost + preparation_cost + extra_cost
29
27
  end
30
28
 
31
29
  private
32
30
 
33
- attr_reader :operation, :signal, :kernel, :plan, :buffers
31
+ attr_reader :operation, :signal, :kernel, :plan, :buffers, :preparation
34
32
 
35
- def dimensions
36
- return linear_dimensions unless plan.wrap?
37
-
38
- circular_dimensions
33
+ def transform_cost
34
+ transform = FftCost.transform(buffers.transform_size, signal.ndim,
35
+ real: !buffers.real_axis.nil?, profile: @profile)
36
+ FftCost.fixed(signal.ndim, profile: @profile) + transform + (AXIS_MOVE_COST * moved_size)
39
37
  end
40
38
 
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)]
39
+ def extra_cost
40
+ correlation_cost = operation == :correlation ? CORRELATION_COST * buffers.spectrum_size : 0.0
41
+ correlation_cost + preparation.conversion + result_cost
44
42
  end
45
43
 
46
- def linear_dimensions
47
- [buffers.transform_size, buffers.spectrum_size,
48
- cost_for(REAL_FFT_COST), 0]
44
+ def result_cost
45
+ RESULT_COST * plan.result_size * (plan.dtype::ELEMENT_BYTE_SIZE / 4)
49
46
  end
50
47
 
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
48
+ def preparation_cost
49
+ return circular_preparation_cost if plan.wrap?
56
50
 
57
- def cost_for(costs)
58
- costs.fetch(signal.ndim, costs.values.last)
51
+ (2 * FftCost::PREPARATION * buffers.transform_size) + preparation.extension
59
52
  end
60
53
 
61
- def transform_coefficient(real_axis)
62
- cost_for(real_axis ? REAL_FFT_COST : COMPLEX_FFT_COST)
54
+ def circular_preparation_cost
55
+ (FftCost::PREPARATION * (signal.size + kernel.size)) + preparation.folding(kernel.size)
63
56
  end
64
57
 
65
- def moved_size(real_axis)
66
- real_axis && real_axis != signal.ndim - 1 ? 3 * signal.size : 0
58
+ def moved_size
59
+ buffers.real_axis && buffers.real_axis != signal.ndim - 1 ? 3 * signal.size : 0
67
60
  end
68
61
  end
69
62
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'convolver/buffer_limits'
4
4
 
5
- # Internal allocation and arithmetic checks for the PocketFFT paths.
5
+ # @!macro convolver_overview
6
6
  module Convolver
7
7
  # Only FFT representability failures permit automatic direct fallback.
8
8
  # @private
@@ -5,6 +5,7 @@ module Convolver
5
5
  # @private
6
6
  class LinearFftOperation
7
7
  def initialize(operation, signal, kernel, plan)
8
+ @dtype = plan.dtype
8
9
  @operation = operation
9
10
  @signal = signal
10
11
  @kernel = kernel
@@ -15,12 +16,13 @@ module Convolver
15
16
  signal_padded = zero_padded(signal)
16
17
  kernel_padded = zero_padded(kernel)
17
18
  full_result = real_spectrum_product(signal_padded, kernel_padded)
18
- Numo::SFloat.cast(full_result[*valid_ranges])
19
+ cropped = full_result[*valid_ranges]
20
+ dtype == Numo::DFloat ? cropped.dup : dtype.cast(cropped)
19
21
  end
20
22
 
21
23
  private
22
24
 
23
- attr_reader :operation, :signal, :kernel, :transform_shape
25
+ attr_reader :operation, :signal, :kernel, :transform_shape, :dtype
24
26
 
25
27
  def zero_padded(value)
26
28
  Numo::DFloat.zeros(*transform_shape).tap do |padded|
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Convolver
4
+ # Resolves public precision without allocating operand copies.
5
+ # @private
6
+ class OperationDtype
7
+ SINGLE_INPUTS = [Numo::SFloat, Numo::Int8, Numo::UInt8, Numo::Int16, Numo::UInt16].freeze
8
+ DOUBLE_INPUTS = [Numo::DFloat, Numo::Int32, Numo::UInt32, Numo::Int64, Numo::UInt64].freeze
9
+ INPUTS = (SINGLE_INPUTS + DOUBLE_INPUTS).freeze
10
+ OUTPUTS = [Numo::SFloat, Numo::DFloat].freeze
11
+
12
+ def self.resolve(signal, kernel, dtype)
13
+ validate_inputs!(signal, kernel)
14
+ validate_dtype!(dtype)
15
+ types = [signal.class, kernel.class]
16
+ dtype || (types.any? { |type| DOUBLE_INPUTS.include?(type) } ? Numo::DFloat : Numo::SFloat)
17
+ end
18
+
19
+ def self.validate_inputs!(signal, kernel)
20
+ unless signal.is_a?(Numo::NArray) && kernel.is_a?(Numo::NArray)
21
+ raise ArgumentError, 'signal and kernel must be Numo::NArray values'
22
+ end
23
+
24
+ return if [signal.class, kernel.class].all? { |type| INPUTS.include?(type) }
25
+
26
+ raise ArgumentError, 'unsupported signal or kernel dtype'
27
+ end
28
+
29
+ def self.validate_dtype!(dtype)
30
+ return if dtype.nil? || OUTPUTS.include?(dtype)
31
+
32
+ raise ArgumentError, 'dtype must be Numo::SFloat, Numo::DFloat, or nil'
33
+ end
34
+
35
+ def self.cast_fill(value, dtype)
36
+ # A scalar IEEE float conversion, without allocating a Numo buffer before preflight.
37
+ dtype == Numo::SFloat ? [value].pack('f').unpack1('f') : value
38
+ end
39
+ end
40
+
41
+ private_constant :OperationDtype
42
+ end