Skip to content

Commit 2dff3fc

Browse files
authored
Fix JAX FFT shift axis normalization (#4959)
* Fix JAX FFT shift axis normalization * Add JAX FFT shift axis regressions
1 parent aacc08c commit 2dff3fc

2 files changed

Lines changed: 80 additions & 2 deletions

File tree

src/pyrecest/_backend/jax/fft.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
_BOOLEAN_FFT_LENGTH_ERROR = "n must be an integer length, not boolean"
1111
_FFT_SHAPE_SEQUENCE_ERROR = "s must be None or a sequence of integer lengths"
1212
_FFT_AXES_SEQUENCE_ERROR = "axes must be None or a sequence of integer axes"
13+
_SHIFT_AXES_ERROR = "axes must be None, an integer axis, or a sequence of integer axes"
1314

1415

1516
def _normalize_real_fft_axis(axis):
@@ -138,6 +139,50 @@ def _normalize_complex_fft_axes(axes):
138139
)
139140

140141

142+
def _normalize_shift_axes(axes):
143+
"""Normalize scalar or sequence axis inputs accepted by FFT shifts."""
144+
if axes is None:
145+
return None
146+
if isinstance(axes, (bool, _np.bool_)):
147+
raise TypeError(_BOOLEAN_FFT_AXIS_ERROR)
148+
if isinstance(axes, _np.ndarray):
149+
if _np.issubdtype(axes.dtype, _np.bool_):
150+
raise TypeError(_BOOLEAN_FFT_AXIS_ERROR)
151+
if axes.ndim == 0:
152+
return _normalize_fft_sequence_item(
153+
axes, _BOOLEAN_FFT_AXIS_ERROR, _SHIFT_AXES_ERROR
154+
)
155+
return tuple(
156+
_normalize_fft_sequence_item(
157+
item, _BOOLEAN_FFT_AXIS_ERROR, _SHIFT_AXES_ERROR
158+
)
159+
for item in axes.tolist()
160+
)
161+
if isinstance(axes, _jnp.ndarray):
162+
axes_array = _np.asarray(axes)
163+
if _np.issubdtype(axes_array.dtype, _np.bool_):
164+
raise TypeError(_BOOLEAN_FFT_AXIS_ERROR)
165+
if axes_array.ndim == 0:
166+
return _normalize_fft_sequence_item(
167+
axes, _BOOLEAN_FFT_AXIS_ERROR, _SHIFT_AXES_ERROR
168+
)
169+
return tuple(
170+
_normalize_fft_sequence_item(
171+
item, _BOOLEAN_FFT_AXIS_ERROR, _SHIFT_AXES_ERROR
172+
)
173+
for item in axes_array.tolist()
174+
)
175+
try:
176+
return _operator_index(axes)
177+
except TypeError:
178+
return _normalize_fft_integer_sequence(
179+
axes,
180+
_SHIFT_AXES_ERROR,
181+
_BOOLEAN_FFT_AXIS_ERROR,
182+
_SHIFT_AXES_ERROR,
183+
)
184+
185+
141186
def rfft(a, n=None, axis=-1, norm=None):
142187
return _fft.rfft(
143188
_jnp.asarray(a),
@@ -175,8 +220,8 @@ def ifftn(a, s=None, axes=None, norm=None):
175220

176221

177222
def fftshift(x, axes=None):
178-
return _fft.fftshift(_jnp.asarray(x), axes=axes)
223+
return _fft.fftshift(_jnp.asarray(x), axes=_normalize_shift_axes(axes))
179224

180225

181226
def ifftshift(x, axes=None):
182-
return _fft.ifftshift(_jnp.asarray(x), axes=axes)
227+
return _fft.ifftshift(_jnp.asarray(x), axes=_normalize_shift_axes(axes))
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import numpy as np
2+
import pytest
3+
4+
jax = pytest.importorskip("jax")
5+
import jax.numpy as jnp # noqa: E402
6+
7+
from pyrecest._backend.jax import fft # noqa: E402
8+
9+
10+
@pytest.mark.parametrize("shift", [fft.fftshift, fft.ifftshift])
11+
def test_shift_accepts_numpy_integer_scalar_array_axis(shift):
12+
values = np.arange(12).reshape(3, 4)
13+
14+
actual = np.asarray(shift(values, axes=np.asarray(1, dtype=np.int64)))
15+
expected = np.asarray(shift(values, axes=1))
16+
17+
np.testing.assert_array_equal(actual, expected)
18+
19+
20+
@pytest.mark.parametrize("shift", [fft.fftshift, fft.ifftshift])
21+
def test_shift_accepts_jax_integer_axis_sequence(shift):
22+
values = np.arange(24).reshape(2, 3, 4)
23+
24+
actual = np.asarray(shift(values, axes=jnp.asarray([0, 2])))
25+
expected = np.asarray(shift(values, axes=(0, 2)))
26+
27+
np.testing.assert_array_equal(actual, expected)
28+
29+
30+
@pytest.mark.parametrize("shift", [fft.fftshift, fft.ifftshift])
31+
def test_shift_rejects_boolean_axes(shift):
32+
with pytest.raises(TypeError, match="axis must be an integer, not boolean"):
33+
shift(np.arange(4), axes=np.asarray(True))

0 commit comments

Comments
 (0)