Skip to content

Commit 52ee3cf

Browse files
committed
refactor and cupy
1 parent 0802b65 commit 52ee3cf

13 files changed

Lines changed: 531 additions & 373 deletions

notebooks/benchmark.ipynb

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 16,
6+
"id": "6194860c",
7+
"metadata": {},
8+
"outputs": [
9+
{
10+
"name": "stdout",
11+
"output_type": "stream",
12+
"text": [
13+
"Found 60 audio files\n",
14+
"Total audio: 2880000 samples (180.0s at 16000Hz)\n"
15+
]
16+
}
17+
],
18+
"source": [
19+
"# Simple benchmark comparing NumPy (CPU) vs CuPy (GPU) performance.\n",
20+
"\n",
21+
"import time\n",
22+
"from pathlib import Path\n",
23+
"import numpy as np\n",
24+
"import soundfile as sf\n",
25+
"import pygaborstm as stm\n",
26+
"\n",
27+
"# Load all 60 ripple files\n",
28+
"data_dir = Path(\"data/mvripfft\")\n",
29+
"audio_files = sorted(data_dir.glob(\"*.wav\"))\n",
30+
"print(f\"Found {len(audio_files)} audio files\")\n",
31+
"\n",
32+
"# Load and concatenate for a longer test signal\n",
33+
"audios = []\n",
34+
"for f in audio_files:\n",
35+
" audio, sr = sf.read(f)\n",
36+
" audios.append(audio)\n",
37+
"\n",
38+
"audio = np.concatenate(audios)\n",
39+
"print(f\"Total audio: {len(audio)} samples ({len(audio)/sr:.1f}s at {sr}Hz)\")"
40+
]
41+
},
42+
{
43+
"cell_type": "code",
44+
"execution_count": 17,
45+
"id": "38c1ede8",
46+
"metadata": {},
47+
"outputs": [
48+
{
49+
"name": "stdout",
50+
"output_type": "stream",
51+
"text": [
52+
"CPU time: 169.165s\n"
53+
]
54+
}
55+
],
56+
"source": [
57+
"# CPU Benchmark\n",
58+
"config_cpu = stm.Config(use_gpu=False, resolution=\"low\")\n",
59+
"model_cpu = stm.PyGaborSTM(config_cpu)\n",
60+
"\n",
61+
"# Warmup\n",
62+
"_ = model_cpu.spectrogram(audios[0])\n",
63+
"\n",
64+
"# Benchmark\n",
65+
"start = time.perf_counter()\n",
66+
"spec_cpu = model_cpu.spectrogram(audio)\n",
67+
"rsf_cpu = model_cpu.rsf(spec_cpu)\n",
68+
"cpu_time = time.perf_counter() - start\n",
69+
"\n",
70+
"print(f\"CPU time: {cpu_time:.3f}s\")"
71+
]
72+
},
73+
{
74+
"cell_type": "code",
75+
"execution_count": 18,
76+
"id": "86bb6abe",
77+
"metadata": {},
78+
"outputs": [
79+
{
80+
"name": "stdout",
81+
"output_type": "stream",
82+
"text": [
83+
"GPU time: 170.680s\n"
84+
]
85+
}
86+
],
87+
"source": [
88+
"# GPU Benchmark\n",
89+
"config_gpu = stm.Config(use_gpu=True, resolution=\"low\")\n",
90+
"model_gpu = stm.PyGaborSTM(config_gpu)\n",
91+
"\n",
92+
"# Warmup (includes kernel compilation)\n",
93+
"_ = model_gpu.spectrogram(audios[0])\n",
94+
"_ = model_gpu.rsf(model_gpu.spectrogram(audios[0]))\n",
95+
"\n",
96+
"# Benchmark\n",
97+
"start = time.perf_counter()\n",
98+
"spec_gpu = model_gpu.spectrogram(audio)\n",
99+
"rsf_gpu = model_gpu.rsf(spec_gpu)\n",
100+
"gpu_time = time.perf_counter() - start\n",
101+
"\n",
102+
"print(f\"GPU time: {gpu_time:.3f}s\")"
103+
]
104+
},
105+
{
106+
"cell_type": "code",
107+
"execution_count": 19,
108+
"id": "a3308e88",
109+
"metadata": {},
110+
"outputs": [
111+
{
112+
"name": "stdout",
113+
"output_type": "stream",
114+
"text": [
115+
"\n",
116+
"========================================\n",
117+
"Audio length: 180.0s\n",
118+
"Resolution: low (60 filters)\n",
119+
"========================================\n",
120+
"CPU time: 169.165s\n",
121+
"GPU time: 170.680s\n",
122+
"Speedup: 1.0x\n",
123+
"========================================\n",
124+
"\n",
125+
"Spectrogram match: True\n",
126+
"RSF match: True\n"
127+
]
128+
}
129+
],
130+
"source": [
131+
"# Results\n",
132+
"print(f\"\\n{'='*40}\")\n",
133+
"print(f\"Audio length: {len(audio)/sr:.1f}s\")\n",
134+
"print(f\"Resolution: low (60 filters)\")\n",
135+
"print(f\"{'='*40}\")\n",
136+
"print(f\"CPU time: {cpu_time:.3f}s\")\n",
137+
"print(f\"GPU time: {gpu_time:.3f}s\")\n",
138+
"print(f\"Speedup: {cpu_time/gpu_time:.1f}x\")\n",
139+
"print(f\"{'='*40}\")\n",
140+
"\n",
141+
"# Verify outputs match\n",
142+
"spec_match = np.allclose(spec_cpu.data, spec_gpu.data, rtol=1e-4)\n",
143+
"rsf_match = np.allclose(rsf_cpu.data, rsf_gpu.data, rtol=1e-4)\n",
144+
"print(f\"\\nSpectrogram match: {spec_match}\")\n",
145+
"print(f\"RSF match: {rsf_match}\")"
146+
]
147+
}
148+
],
149+
"metadata": {
150+
"kernelspec": {
151+
"display_name": "Python 3 (ipykernel)",
152+
"language": "python",
153+
"name": "python3"
154+
},
155+
"language_info": {
156+
"codemirror_mode": {
157+
"name": "ipython",
158+
"version": 3
159+
},
160+
"file_extension": ".py",
161+
"mimetype": "text/x-python",
162+
"name": "python",
163+
"nbconvert_exporter": "python",
164+
"pygments_lexer": "ipython3",
165+
"version": "3.12.7"
166+
}
167+
},
168+
"nbformat": 4,
169+
"nbformat_minor": 5
170+
}

notebooks/chi2005_validation.ipynb

Lines changed: 39 additions & 28 deletions
Large diffs are not rendered by default.

notebooks/example_usage.ipynb

Lines changed: 23 additions & 54 deletions
Large diffs are not rendered by default.

notebooks/mvripfft_validation.ipynb

Lines changed: 43 additions & 9 deletions
Large diffs are not rendered by default.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
# Defaults
1212
SR = 16000
13-
DURATION = 1.0
13+
DURATION = 3.0
1414

1515

1616
def generate_tone(freq, duration=DURATION, sr=SR, amplitude=0.5):

pygaborstm/__init__.py

Lines changed: 28 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,38 @@
1-
from .config import Config, SpectrogramConfig, GaborConfig
1+
"""
2+
PyGaborSTM: Spectro-temporal modulation analysis library.
3+
4+
Usage:
5+
import pygaborstm as stm
6+
7+
# Create model
8+
model = stm.PyGaborSTM(config=stm.Config(use_gpu=True))
9+
10+
# Compute
11+
spec = model.spectrogram(audio)
12+
rsf = model.rsf(spec)
13+
14+
# Plot
15+
stm.plot.spectrogram(spec)
16+
stm.plot.rsf(rsf)
17+
"""
18+
19+
from .config import Config
220
from .structs import Spectrogram, RSF
3-
from .spectrogram import AuditorySpectrogram, auditory_spectrogram
4-
from .gabor import GaborFilterbank, rsf
5-
from .core import load, compute_rsf
6-
from .plotting import (
7-
plot_spectrogram,
8-
plot_spectrogram_grid,
9-
plot_rsf,
10-
plot_rsf_grid,
11-
plot_filterbank,
12-
)
13-
from .utils import (
14-
generate_tone,
15-
generate_three_tones,
16-
generate_broadband_noise,
17-
generate_harmonic_complex,
18-
generate_moving_ripple,
19-
generate_ripple_set,
20-
save_three_tones,
21-
save_noise,
22-
save_harmonic_complexes,
23-
)
21+
from .core import PyGaborSTM
22+
from . import plot
23+
from . import structs
2424

2525
__version__ = "0.1.0"
2626

2727
__all__ = [
28-
# Core functions
29-
"load",
30-
"compute_rsf",
31-
# Functional API
32-
"auditory_spectrogram",
33-
"rsf",
34-
# Classes
35-
"AuditorySpectrogram",
36-
"GaborFilterbank",
28+
# Main class
29+
"PyGaborSTM",
3730
# Config
3831
"Config",
39-
"SpectrogramConfig",
40-
"GaborConfig",
4132
# Data structures
4233
"Spectrogram",
4334
"RSF",
44-
# Plotting
45-
"plot_spectrogram",
46-
"plot_spectrogram_grid",
47-
"plot_rsf",
48-
"plot_rsf_grid",
49-
"plot_filterbank",
50-
# Stimulus generation
51-
"generate_tone",
52-
"generate_three_tones",
53-
"generate_broadband_noise",
54-
"generate_harmonic_complex",
55-
"generate_moving_ripple",
56-
"generate_ripple_set",
57-
"save_three_tones",
58-
"save_noise",
59-
"save_harmonic_complexes",
60-
]
35+
# Namespaced modules
36+
"plot",
37+
"structs"
38+
]

pygaborstm/backend.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Backend module for numpy/cupy switching.
3+
4+
Provides a unified interface for array operations that can use either
5+
numpy (CPU) or cupy (GPU) depending on configuration.
6+
"""
7+
8+
import numpy as np
9+
10+
# Try to import cupy
11+
try:
12+
import cupy as cp
13+
import cupyx.scipy.signal as cp_signal
14+
CUPY_AVAILABLE = True
15+
except ImportError:
16+
cp = None
17+
cp_signal = None
18+
CUPY_AVAILABLE = False
19+
20+
21+
def get_array_module(use_gpu: bool = False):
22+
"""
23+
Get the appropriate array module (numpy or cupy).
24+
25+
Args:
26+
use_gpu: Whether to use GPU acceleration
27+
28+
Returns:
29+
Module (numpy or cupy)
30+
"""
31+
if use_gpu:
32+
if not CUPY_AVAILABLE:
33+
import warnings
34+
warnings.warn(
35+
"CuPy not available. Falling back to NumPy. "
36+
"Install cupy-cuda13x for GPU acceleration.",
37+
UserWarning
38+
)
39+
return np
40+
return cp
41+
return np
42+
43+
44+
def get_signal_module(use_gpu: bool = False):
45+
"""
46+
Get the appropriate signal processing module.
47+
48+
Args:
49+
use_gpu: Whether to use GPU acceleration
50+
51+
Returns:
52+
Module (scipy.signal or cupyx.scipy.signal)
53+
"""
54+
if use_gpu:
55+
if not CUPY_AVAILABLE:
56+
from scipy import signal
57+
return signal
58+
return cp_signal
59+
from scipy import signal
60+
return signal
61+
62+
63+
def to_numpy(array) -> np.ndarray:
64+
"""
65+
Convert array to numpy (transfers from GPU if needed).
66+
67+
Args:
68+
array: numpy or cupy array
69+
70+
Returns:
71+
numpy array
72+
"""
73+
if CUPY_AVAILABLE and isinstance(array, cp.ndarray):
74+
return cp.asnumpy(array)
75+
return np.asarray(array)

pygaborstm/config.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,21 @@
88

99

1010
@dataclass
11-
class SpectrogramConfig:
12-
"""Configuration for auditory spectrogram."""
13-
11+
class Config:
12+
"""Configuration for PyGaborSTM."""
13+
14+
# General
15+
use_gpu: bool = False
1416
sample_rate: int = 16000
17+
18+
# Spectrogram
1519
n_filters: int = 256
1620
f_min: float = 180.0
1721
octaves: float = 5.3
1822
tau_ms: float = 4.0
1923
frmlen_ms: float = 8.0
20-
constant_Q: float = 16.0
21-
22-
23-
@dataclass
24-
class GaborConfig:
25-
"""Configuration for Gabor filterbank."""
26-
27-
sample_rate: int = 16000
28-
n_freq_bins: int = 128
24+
25+
# RSF / Gabor
2926
resolution: str = "low"
3027
rsf_frame_size_ms: int = 500
3128
rsf_frame_shift_ms: int = 10
32-
33-
34-
@dataclass
35-
class Config:
36-
"""Top-level configuration."""
37-
38-
use_gpu: bool = False
39-
spectrogram: SpectrogramConfig = field(default_factory=SpectrogramConfig)
40-
gabor: GaborConfig = field(default_factory=GaborConfig)

0 commit comments

Comments
 (0)