gpuarray 0.3.0__tar.gz → 0.4.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gpuarray
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: NumPy-like GPU array library that works on every GPU — NVIDIA, AMD, Intel, Apple Silicon. No CUDA required.
5
5
  Author: Arnav Kewalram
6
6
  License: MIT
@@ -27,6 +27,10 @@ from .api import (
27
27
  concatenate,
28
28
  stack,
29
29
  random,
30
+ fft,
31
+ ifft,
32
+ fft_freq,
33
+ fft_magnitude,
30
34
  )
31
35
 
32
36
  __version__ = "0.1.0"
@@ -58,4 +62,8 @@ __all__ = [
58
62
  "concatenate",
59
63
  "stack",
60
64
  "random",
65
+ "fft",
66
+ "ifft",
67
+ "fft_freq",
68
+ "fft_magnitude",
61
69
  ]
@@ -6,7 +6,7 @@ from typing import Sequence
6
6
 
7
7
  import numpy as np
8
8
 
9
- from .core import GPUArray, _from_numpy, matmul as _matmul
9
+ from .core import GPUArray, _from_numpy, matmul as _matmul, _complex_magnitude
10
10
  from . import shaders
11
11
 
12
12
 
@@ -180,3 +180,37 @@ class _Random:
180
180
 
181
181
 
182
182
  random = _Random()
183
+
184
+
185
+ # ----- FFT functions -----
186
+
187
+ def fft(a) -> GPUArray:
188
+ """1D FFT. Input is real or complex (interleaved). Returns complex GPUArray."""
189
+ if not isinstance(a, GPUArray):
190
+ a = array(a)
191
+ return a.fft()
192
+
193
+
194
+ def ifft(a) -> GPUArray:
195
+ """1D inverse FFT. Input is complex interleaved. Returns complex GPUArray."""
196
+ if not isinstance(a, GPUArray):
197
+ a = array(a)
198
+ return a.ifft()
199
+
200
+
201
+ def fft_freq(n: int, d: float = 1.0) -> GPUArray:
202
+ """Return the DFT sample frequencies (like numpy.fft.fftfreq)."""
203
+ results = np.empty(n, dtype=np.float32)
204
+ N = (n - 1) // 2 + 1
205
+ results[:N] = np.arange(0, N, dtype=np.float32)
206
+ results[N:] = np.arange(-(n // 2), 0, dtype=np.float32)
207
+ results /= (n * d)
208
+ return _from_numpy(results)
209
+
210
+
211
+ def fft_magnitude(a) -> GPUArray:
212
+ """Compute magnitude spectrum |FFT(x)|. Returns real GPUArray."""
213
+ if not isinstance(a, GPUArray):
214
+ a = array(a)
215
+ fft_result = a.fft()
216
+ return _complex_magnitude(fft_result)
@@ -658,6 +658,224 @@ class GPUArray:
658
658
  return self._shape[0]
659
659
 
660
660
 
661
+ # ----- FFT methods -----
662
+
663
+ def fft(self) -> "GPUArray":
664
+ """1D FFT. Input is real or complex (interleaved). Returns complex GPUArray (interleaved)."""
665
+ return _fft_impl(self, inverse=False)
666
+
667
+ def ifft(self) -> "GPUArray":
668
+ """1D inverse FFT. Input is complex interleaved. Returns complex GPUArray."""
669
+ return _fft_impl(self, inverse=True)
670
+
671
+ def fft_magnitude(self) -> "GPUArray":
672
+ """Compute magnitude spectrum |FFT(x)|. Returns real GPUArray."""
673
+ fft_result = self.fft()
674
+ return _complex_magnitude(fft_result)
675
+
676
+ def fft_phase(self) -> "GPUArray":
677
+ """Compute phase spectrum angle(FFT(x)). Returns real GPUArray."""
678
+ fft_result = self.fft()
679
+ return _complex_phase(fft_result)
680
+
681
+
682
+ def _next_power_of_2(n: int) -> int:
683
+ """Return the smallest power of 2 >= n."""
684
+ if n <= 1:
685
+ return 1
686
+ p = 1
687
+ while p < n:
688
+ p <<= 1
689
+ return p
690
+
691
+
692
+ def _fft_impl(arr: GPUArray, inverse: bool = False) -> GPUArray:
693
+ """Cooley-Tukey radix-2 FFT/IFFT on GPU."""
694
+ device = get_device()
695
+
696
+ # Determine if input is real or complex (interleaved)
697
+ # Real: 1D array of N floats -> N-point FFT
698
+ # Complex interleaved: 1D array of 2*N floats -> N-point FFT
699
+ # We treat it as real if ndim==1 and size is the number of points
700
+ # For simplicity: if the array was produced by a previous FFT (even size),
701
+ # the caller should know. We'll check: if doing IFFT, input must be complex.
702
+
703
+ flat = arr.to_numpy().flatten()
704
+
705
+ if inverse:
706
+ # Input must be complex interleaved (even number of floats)
707
+ assert len(flat) % 2 == 0, "IFFT input must be complex interleaved (even length)"
708
+ N_complex = len(flat) // 2
709
+ else:
710
+ # Assume real input -> convert to complex
711
+ N_complex = len(flat)
712
+
713
+ # Pad to next power of 2
714
+ N = _next_power_of_2(N_complex)
715
+ log2N = int(math.log2(N))
716
+
717
+ if inverse:
718
+ # Pad complex data
719
+ if N > N_complex:
720
+ padded = np.zeros(2 * N, dtype=np.float32)
721
+ padded[:len(flat)] = flat
722
+ else:
723
+ padded = flat.copy()
724
+ complex_data = padded
725
+ else:
726
+ # Convert real to complex interleaved
727
+ padded_real = np.zeros(N, dtype=np.float32)
728
+ padded_real[:len(flat)] = flat
729
+ complex_data = np.zeros(2 * N, dtype=np.float32)
730
+ complex_data[0::2] = padded_real # real parts
731
+ # imaginary parts are already 0
732
+
733
+ # Upload complex data to GPU
734
+ input_buf = device.create_buffer_with_data(
735
+ data=complex_data.tobytes(),
736
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
737
+ )
738
+
739
+ # Step 1: Bit-reversal permutation
740
+ output_buf = device.create_buffer(
741
+ size=2 * N * 4,
742
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
743
+ )
744
+
745
+ _, br_pipeline = _get_pipeline(shaders.FFT_BIT_REVERSE_SHADER)
746
+ params_data = struct.pack("IIII", N, log2N, 0, 0)
747
+ params_buf = device.create_buffer_with_data(
748
+ data=params_data, usage=wgpu.BufferUsage.UNIFORM,
749
+ )
750
+ bind_group = device.create_bind_group(
751
+ layout=br_pipeline.get_bind_group_layout(0),
752
+ entries=[
753
+ {"binding": 0, "resource": {"buffer": input_buf, "offset": 0, "size": 2 * N * 4}},
754
+ {"binding": 1, "resource": {"buffer": output_buf, "offset": 0, "size": 2 * N * 4}},
755
+ {"binding": 2, "resource": {"buffer": params_buf, "offset": 0, "size": 16}},
756
+ ],
757
+ )
758
+ n_workgroups = math.ceil(N / WORKGROUP_SIZE)
759
+ encoder = device.create_command_encoder()
760
+ pass_enc = encoder.begin_compute_pass()
761
+ pass_enc.set_pipeline(br_pipeline)
762
+ pass_enc.set_bind_group(0, bind_group)
763
+ pass_enc.dispatch_workgroups(n_workgroups)
764
+ pass_enc.end()
765
+ device.queue.submit([encoder.finish()])
766
+ input_buf.destroy()
767
+ params_buf.destroy()
768
+
769
+ # Step 2: Butterfly stages
770
+ butterfly_shader = shaders.IFFT_BUTTERFLY_SHADER if inverse else shaders.FFT_BUTTERFLY_SHADER
771
+ _, bf_pipeline = _get_pipeline(butterfly_shader)
772
+
773
+ for stage in range(log2N):
774
+ params_data = struct.pack("IIII", N, stage, 0, 0)
775
+ params_buf = device.create_buffer_with_data(
776
+ data=params_data, usage=wgpu.BufferUsage.UNIFORM,
777
+ )
778
+ bind_group = device.create_bind_group(
779
+ layout=bf_pipeline.get_bind_group_layout(0),
780
+ entries=[
781
+ {"binding": 0, "resource": {"buffer": output_buf, "offset": 0, "size": 2 * N * 4}},
782
+ {"binding": 1, "resource": {"buffer": params_buf, "offset": 0, "size": 16}},
783
+ ],
784
+ )
785
+ n_workgroups_bf = math.ceil(N // 2 / WORKGROUP_SIZE)
786
+ if n_workgroups_bf == 0:
787
+ n_workgroups_bf = 1
788
+ encoder = device.create_command_encoder()
789
+ pass_enc = encoder.begin_compute_pass()
790
+ pass_enc.set_pipeline(bf_pipeline)
791
+ pass_enc.set_bind_group(0, bind_group)
792
+ pass_enc.dispatch_workgroups(n_workgroups_bf)
793
+ pass_enc.end()
794
+ device.queue.submit([encoder.finish()])
795
+ params_buf.destroy()
796
+
797
+ # Step 3: For IFFT, scale by 1/N
798
+ if inverse:
799
+ _, scale_pipeline = _get_pipeline(shaders.FFT_SCALE_SHADER)
800
+ scale_val = 1.0 / float(N)
801
+ params_data = struct.pack("f", scale_val) + b"\x00" * 12
802
+ params_buf = device.create_buffer_with_data(
803
+ data=params_data, usage=wgpu.BufferUsage.UNIFORM,
804
+ )
805
+ bind_group = device.create_bind_group(
806
+ layout=scale_pipeline.get_bind_group_layout(0),
807
+ entries=[
808
+ {"binding": 0, "resource": {"buffer": output_buf, "offset": 0, "size": 2 * N * 4}},
809
+ {"binding": 1, "resource": {"buffer": params_buf, "offset": 0, "size": 16}},
810
+ ],
811
+ )
812
+ n_workgroups_sc = math.ceil(2 * N / WORKGROUP_SIZE)
813
+ encoder = device.create_command_encoder()
814
+ pass_enc = encoder.begin_compute_pass()
815
+ pass_enc.set_pipeline(scale_pipeline)
816
+ pass_enc.set_bind_group(0, bind_group)
817
+ pass_enc.dispatch_workgroups(n_workgroups_sc)
818
+ pass_enc.end()
819
+ device.queue.submit([encoder.finish()])
820
+ params_buf.destroy()
821
+
822
+ return GPUArray(output_buf, (2 * N,))
823
+
824
+
825
+ def _complex_magnitude(arr: GPUArray) -> GPUArray:
826
+ """Compute magnitude of complex interleaved array."""
827
+ device = get_device()
828
+ N = arr.size // 2
829
+ _, pipeline = _get_pipeline(shaders.COMPLEX_MAGNITUDE_SHADER)
830
+ out_buf = device.create_buffer(
831
+ size=N * 4,
832
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
833
+ )
834
+ bind_group = device.create_bind_group(
835
+ layout=pipeline.get_bind_group_layout(0),
836
+ entries=[
837
+ {"binding": 0, "resource": {"buffer": arr._buffer, "offset": 0, "size": arr.size * 4}},
838
+ {"binding": 1, "resource": {"buffer": out_buf, "offset": 0, "size": N * 4}},
839
+ ],
840
+ )
841
+ n_workgroups = math.ceil(N / WORKGROUP_SIZE)
842
+ encoder = device.create_command_encoder()
843
+ pass_enc = encoder.begin_compute_pass()
844
+ pass_enc.set_pipeline(pipeline)
845
+ pass_enc.set_bind_group(0, bind_group)
846
+ pass_enc.dispatch_workgroups(n_workgroups)
847
+ pass_enc.end()
848
+ device.queue.submit([encoder.finish()])
849
+ return GPUArray(out_buf, (N,))
850
+
851
+
852
+ def _complex_phase(arr: GPUArray) -> GPUArray:
853
+ """Compute phase of complex interleaved array."""
854
+ device = get_device()
855
+ N = arr.size // 2
856
+ _, pipeline = _get_pipeline(shaders.COMPLEX_PHASE_SHADER)
857
+ out_buf = device.create_buffer(
858
+ size=N * 4,
859
+ usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST,
860
+ )
861
+ bind_group = device.create_bind_group(
862
+ layout=pipeline.get_bind_group_layout(0),
863
+ entries=[
864
+ {"binding": 0, "resource": {"buffer": arr._buffer, "offset": 0, "size": arr.size * 4}},
865
+ {"binding": 1, "resource": {"buffer": out_buf, "offset": 0, "size": N * 4}},
866
+ ],
867
+ )
868
+ n_workgroups = math.ceil(N / WORKGROUP_SIZE)
869
+ encoder = device.create_command_encoder()
870
+ pass_enc = encoder.begin_compute_pass()
871
+ pass_enc.set_pipeline(pipeline)
872
+ pass_enc.set_bind_group(0, bind_group)
873
+ pass_enc.dispatch_workgroups(n_workgroups)
874
+ pass_enc.end()
875
+ device.queue.submit([encoder.finish()])
876
+ return GPUArray(out_buf, (N,))
877
+
878
+
661
879
  def _from_numpy(data: np.ndarray) -> GPUArray:
662
880
  """Create a GPUArray from a numpy array."""
663
881
  device = get_device()
@@ -879,3 +879,210 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
879
879
  if (i < arrayLength(&a)) { result[i] = a[i] / b[i / dims.cols]; }
880
880
  }
881
881
  """
882
+
883
+ # ---------------------------------------------------------------------------
884
+ # FFT shaders (Cooley-Tukey radix-2)
885
+ # Complex numbers stored as interleaved float32 pairs: [re0, im0, re1, im1, ...]
886
+ # ---------------------------------------------------------------------------
887
+
888
+ REAL_TO_COMPLEX_SHADER = """
889
+ @group(0) @binding(0) var<storage, read> input: array<f32>;
890
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>;
891
+
892
+ @compute @workgroup_size(256)
893
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
894
+ let i = gid.x;
895
+ let n = arrayLength(&input);
896
+ if (i < n) {
897
+ output[i * 2u] = input[i];
898
+ output[i * 2u + 1u] = 0.0;
899
+ }
900
+ }
901
+ """
902
+
903
+ FFT_BIT_REVERSE_SHADER = """
904
+ struct Params {
905
+ N: u32,
906
+ log2N: u32,
907
+ _pad1: u32,
908
+ _pad2: u32,
909
+ }
910
+
911
+ @group(0) @binding(0) var<storage, read> input: array<f32>;
912
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>;
913
+ @group(0) @binding(2) var<uniform> params: Params;
914
+
915
+ fn bit_reverse(x: u32, bits: u32) -> u32 {
916
+ var v = x;
917
+ var r: u32 = 0u;
918
+ for (var i: u32 = 0u; i < bits; i = i + 1u) {
919
+ r = (r << 1u) | (v & 1u);
920
+ v = v >> 1u;
921
+ }
922
+ return r;
923
+ }
924
+
925
+ @compute @workgroup_size(256)
926
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
927
+ let i = gid.x;
928
+ if (i >= params.N) {
929
+ return;
930
+ }
931
+ let j = bit_reverse(i, params.log2N);
932
+ output[j * 2u] = input[i * 2u];
933
+ output[j * 2u + 1u] = input[i * 2u + 1u];
934
+ }
935
+ """
936
+
937
+ FFT_BUTTERFLY_SHADER = """
938
+ struct Params {
939
+ N: u32,
940
+ stage: u32,
941
+ _pad1: u32,
942
+ _pad2: u32,
943
+ }
944
+
945
+ @group(0) @binding(0) var<storage, read_write> data: array<f32>;
946
+ @group(0) @binding(1) var<uniform> params: Params;
947
+
948
+ @compute @workgroup_size(256)
949
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
950
+ let i = gid.x;
951
+ let m = 1u << (params.stage + 1u);
952
+ let half = m >> 1u;
953
+
954
+ // Total number of butterflies is N/2
955
+ if (i >= params.N / 2u) {
956
+ return;
957
+ }
958
+
959
+ // Which butterfly group and position within group
960
+ let group = i / half;
961
+ let pos = i % half;
962
+ let j = group * m + pos;
963
+ let k = j + half;
964
+
965
+ let pi = 3.14159265358979323846;
966
+ let angle = -2.0 * pi * f32(pos) / f32(m);
967
+ let tw_re = cos(angle);
968
+ let tw_im = sin(angle);
969
+
970
+ // Load X[k]
971
+ let xk_re = data[k * 2u];
972
+ let xk_im = data[k * 2u + 1u];
973
+
974
+ // Twiddle multiply: W * X[k]
975
+ let t_re = tw_re * xk_re - tw_im * xk_im;
976
+ let t_im = tw_re * xk_im + tw_im * xk_re;
977
+
978
+ // Load X[j]
979
+ let xj_re = data[j * 2u];
980
+ let xj_im = data[j * 2u + 1u];
981
+
982
+ // Butterfly
983
+ data[j * 2u] = xj_re + t_re;
984
+ data[j * 2u + 1u] = xj_im + t_im;
985
+ data[k * 2u] = xj_re - t_re;
986
+ data[k * 2u + 1u] = xj_im - t_im;
987
+ }
988
+ """
989
+
990
+ IFFT_BUTTERFLY_SHADER = """
991
+ struct Params {
992
+ N: u32,
993
+ stage: u32,
994
+ _pad1: u32,
995
+ _pad2: u32,
996
+ }
997
+
998
+ @group(0) @binding(0) var<storage, read_write> data: array<f32>;
999
+ @group(0) @binding(1) var<uniform> params: Params;
1000
+
1001
+ @compute @workgroup_size(256)
1002
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
1003
+ let i = gid.x;
1004
+ let m = 1u << (params.stage + 1u);
1005
+ let half = m >> 1u;
1006
+
1007
+ if (i >= params.N / 2u) {
1008
+ return;
1009
+ }
1010
+
1011
+ let group = i / half;
1012
+ let pos = i % half;
1013
+ let j = group * m + pos;
1014
+ let k = j + half;
1015
+
1016
+ let pi = 3.14159265358979323846;
1017
+ // Conjugate twiddle for IFFT: positive angle
1018
+ let angle = 2.0 * pi * f32(pos) / f32(m);
1019
+ let tw_re = cos(angle);
1020
+ let tw_im = sin(angle);
1021
+
1022
+ let xk_re = data[k * 2u];
1023
+ let xk_im = data[k * 2u + 1u];
1024
+
1025
+ let t_re = tw_re * xk_re - tw_im * xk_im;
1026
+ let t_im = tw_re * xk_im + tw_im * xk_re;
1027
+
1028
+ let xj_re = data[j * 2u];
1029
+ let xj_im = data[j * 2u + 1u];
1030
+
1031
+ data[j * 2u] = xj_re + t_re;
1032
+ data[j * 2u + 1u] = xj_im + t_im;
1033
+ data[k * 2u] = xj_re - t_re;
1034
+ data[k * 2u + 1u] = xj_im - t_im;
1035
+ }
1036
+ """
1037
+
1038
+ FFT_SCALE_SHADER = """
1039
+ struct Params {
1040
+ scalar: f32,
1041
+ _pad1: u32,
1042
+ _pad2: u32,
1043
+ _pad3: u32,
1044
+ }
1045
+
1046
+ @group(0) @binding(0) var<storage, read_write> data: array<f32>;
1047
+ @group(0) @binding(1) var<uniform> params: Params;
1048
+
1049
+ @compute @workgroup_size(256)
1050
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
1051
+ let i = gid.x;
1052
+ if (i < arrayLength(&data)) {
1053
+ data[i] = data[i] * params.scalar;
1054
+ }
1055
+ }
1056
+ """
1057
+
1058
+ COMPLEX_MAGNITUDE_SHADER = """
1059
+ @group(0) @binding(0) var<storage, read> input: array<f32>;
1060
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>;
1061
+
1062
+ @compute @workgroup_size(256)
1063
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
1064
+ let i = gid.x;
1065
+ let n = arrayLength(&output);
1066
+ if (i < n) {
1067
+ let re = input[i * 2u];
1068
+ let im = input[i * 2u + 1u];
1069
+ output[i] = sqrt(re * re + im * im);
1070
+ }
1071
+ }
1072
+ """
1073
+
1074
+ COMPLEX_PHASE_SHADER = """
1075
+ @group(0) @binding(0) var<storage, read> input: array<f32>;
1076
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>;
1077
+
1078
+ @compute @workgroup_size(256)
1079
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
1080
+ let i = gid.x;
1081
+ let n = arrayLength(&output);
1082
+ if (i < n) {
1083
+ let re = input[i * 2u];
1084
+ let im = input[i * 2u + 1u];
1085
+ output[i] = atan2(im, re);
1086
+ }
1087
+ }
1088
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gpuarray
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: NumPy-like GPU array library that works on every GPU — NVIDIA, AMD, Intel, Apple Silicon. No CUDA required.
5
5
  Author: Arnav Kewalram
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gpuarray"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "NumPy-like GPU array library that works on every GPU — NVIDIA, AMD, Intel, Apple Silicon. No CUDA required."
9
9
  requires-python = ">=3.10,<3.14"
10
10
  license = {text = "MIT"}
@@ -557,3 +557,64 @@ class TestBroadcast:
557
557
  result = A + b
558
558
  expected = A_np + b_np
559
559
  npt.assert_allclose(result.to_numpy(), expected, rtol=RTOL)
560
+
561
+
562
+ class TestFFT:
563
+ def test_fft_simple(self):
564
+ """FFT of [1, 0, 0, 0] should be [1, 1, 1, 1] (all real)."""
565
+ signal = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float32)
566
+ gpu_result = gp.fft(gp.array(signal)).to_numpy()
567
+ np_result = np.fft.fft(signal)
568
+ # gpu_result is interleaved [re0, im0, re1, im1, ...]
569
+ gpu_re = gpu_result[0::2]
570
+ gpu_im = gpu_result[1::2]
571
+ npt.assert_allclose(gpu_re, np_result.real, rtol=1e-3, atol=1e-5)
572
+ npt.assert_allclose(gpu_im, np_result.imag, atol=1e-5)
573
+
574
+ def test_fft_sine(self):
575
+ """FFT of a sine wave should have peak at the frequency."""
576
+ N = 256
577
+ freq = 10
578
+ t = np.linspace(0, 1, N, endpoint=False, dtype=np.float32)
579
+ signal = np.sin(2 * np.pi * freq * t).astype(np.float32)
580
+
581
+ gpu_mag = gp.fft_magnitude(gp.array(signal)).to_numpy()
582
+ np_mag = np.abs(np.fft.fft(signal)).astype(np.float32)
583
+ npt.assert_allclose(gpu_mag, np_mag, rtol=1e-3, atol=1e-2)
584
+
585
+ def test_ifft_roundtrip(self):
586
+ """ifft(fft(x)) should equal x."""
587
+ signal = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=np.float32)
588
+ gpu_arr = gp.array(signal)
589
+ fft_result = gp.fft(gpu_arr)
590
+ ifft_result = gp.ifft(fft_result)
591
+ # Extract real parts from interleaved complex result
592
+ recovered = ifft_result.to_numpy()
593
+ recovered_re = recovered[0::2]
594
+ # Should match original (padded to power-of-2 if needed, but 8 is already power of 2)
595
+ npt.assert_allclose(recovered_re[:len(signal)], signal, rtol=1e-3, atol=1e-5)
596
+
597
+ def test_fft_power_of_2(self):
598
+ """Test with exact power of 2 input."""
599
+ N = 16
600
+ signal = np.random.randn(N).astype(np.float32)
601
+ gpu_result = gp.fft(gp.array(signal)).to_numpy()
602
+ np_result = np.fft.fft(signal)
603
+ gpu_re = gpu_result[0::2]
604
+ gpu_im = gpu_result[1::2]
605
+ npt.assert_allclose(gpu_re, np_result.real.astype(np.float32), rtol=1e-3, atol=1e-4)
606
+ npt.assert_allclose(gpu_im, np_result.imag.astype(np.float32), rtol=1e-3, atol=1e-4)
607
+
608
+ def test_fft_freq(self):
609
+ """Test frequency bin generation matches numpy."""
610
+ for n in [8, 16, 32]:
611
+ gpu_freqs = gp.fft_freq(n).to_numpy()
612
+ np_freqs = np.fft.fftfreq(n).astype(np.float32)
613
+ npt.assert_allclose(gpu_freqs, np_freqs, rtol=1e-5)
614
+
615
+ def test_fft_magnitude_api(self):
616
+ """Test fft_magnitude module-level function."""
617
+ signal = np.array([1.0, 0.0, 1.0, 0.0], dtype=np.float32)
618
+ gpu_mag = gp.fft_magnitude(gp.array(signal)).to_numpy()
619
+ np_mag = np.abs(np.fft.fft(signal)).astype(np.float32)
620
+ npt.assert_allclose(gpu_mag, np_mag, rtol=1e-3, atol=1e-5)
File without changes
File without changes
File without changes