Skip to content

Commit ef16b15

Browse files
committed
fix: changelog and tests
1 parent db030ba commit ef16b15

2 files changed

Lines changed: 92 additions & 42 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
* Pinned Cython in the Coverity Scan workflow so generated code stays stable between scans, and added `coverity/README.md` documenting the known false-positive families and the scan review checklist [gh-374](https://github.com/IntelPython/mkl_fft/pull/374)
1515

1616
### Fixed
17-
* Fixed `norm="forward"`/`"ortho"` scaling in `fftn`, `ifftn`, `rfftn`, `irfftn` and the `fft2` family when only a subset of axes is transformed: the scale used the full array shape instead of the transformed axes. The `numpy_fft` and `scipy_fft` interfaces were unaffected [gh-370](https://github.com/IntelPython/mkl_fft/pull/370)
17+
* Fixed `norm="forward"`/`"ortho"` scaling in `fftn`, `ifftn`, `rfftn`, `irfftn` and the `fft2` family when only a subset of axes is transformed: the scale used the full array shape instead of the transformed axes [gh-336](https://github.com/IntelPython/mkl_fft/issues/336), [gh-370](https://github.com/IntelPython/mkl_fft/pull/370)
1818
* Fixed `norm="forward"`/`"ortho"` scaling in `irfftn` and `irfft2`, which normalized over the input length `n` rather than the complex-to-real output length `2 * (n - 1)` [gh-370](https://github.com/IntelPython/mkl_fft/pull/370)
1919
* Declared `f_ndim` as a C `int` in `_allocate_result` so the buffer size is computed in C rather than through a Python object, resolving a Coverity out-of-bounds (OVERRUN) false positive [gh-364](https://github.com/IntelPython/mkl_fft/pull/364)
2020
* Silenced a Coverity `UNUSED_VALUE` finding in `__create_descriptor_1d` by marking the `DftiFreeDescriptor` status (used only by a debug-only `assert`) as intentionally unused [gh-365](https://github.com/IntelPython/mkl_fft/pull/365)

‎mkl_fft/tests/test_dispatch_equivalence.py‎

Lines changed: 91 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,20 @@
5959
_SHAPE_4D = (4, 5, 6, 7)
6060

6161
_DTYPES = ["float32", "float64", "complex64", "complex128"]
62-
_REAL_DTYPES = ["float32", "float64"]
6362

6463
_ORDERS = ["C", "F", "non-contiguous"]
6564

65+
66+
def _cases(funcs, dtypes=_DTYPES):
67+
"""(func, dtype) pairs, dropping the real-input transforms on complex."""
68+
return [
69+
(f, dt)
70+
for f in funcs
71+
for dt in dtypes
72+
if not (f.startswith("rfft") and np.dtype(dt).kind == "c")
73+
]
74+
75+
6676
# Relative tolerance by input precision. Single-precision transforms of
6777
# random data over these lengths stay well inside 2e-5.
6878
_TOL = {
@@ -72,10 +82,22 @@
7282
"complex128": 1e-12,
7383
}
7484

75-
# every non-empty subset of the axes of a 3-D array, plus None
76-
_AXES_3D = [
77-
ax for n in (1, 2, 3) for ax in itertools.combinations(range(3), n)
78-
] + [None]
85+
_NEG_AXES_3D = [
86+
(-1,), # last axis
87+
(-2,), # middle axis: per-vector path in the C backend
88+
(-3,), # first axis
89+
(-2, -1), # the fft2 default
90+
(-3, -1),
91+
(-1, -2), # reversed: picks a different last axis for r2c
92+
(0, -1), # mixed sign
93+
(-3, -2, -1),
94+
]
95+
96+
_AXES_3D = (
97+
[ax for n in (1, 2, 3) for ax in itertools.combinations(range(3), n)]
98+
+ _NEG_AXES_3D
99+
+ [None] # every axis
100+
)
79101

80102

81103
def _make(shape, dtype, seed=42):
@@ -122,13 +144,10 @@ def test_fftn_axes_subset(func, dtype, axes, order):
122144
_check(got, want, dtype)
123145

124146

125-
@pytest.mark.parametrize("func", ["rfftn", "irfftn"])
126-
@pytest.mark.parametrize("dtype", _DTYPES)
147+
@pytest.mark.parametrize("func,dtype", _cases(["rfftn", "irfftn"]))
127148
@pytest.mark.parametrize("axes", _AXES_3D)
128149
@pytest.mark.parametrize("order", _ORDERS)
129150
def test_rfftn_axes_subset(func, dtype, axes, order):
130-
if func == "rfftn" and dtype not in _REAL_DTYPES:
131-
pytest.skip("rfftn takes real input")
132151
x = _relayout(_make(_SHAPE_3D, dtype), order)
133152
got = getattr(mkl_fft, func)(x, axes=axes)
134153
want = getattr(np.fft, func)(x, axes=axes)
@@ -151,29 +170,25 @@ def test_fft_axis_3d(func, dtype, axis, order):
151170
_check(got, want, dtype)
152171

153172

154-
@pytest.mark.parametrize("func", ["fft", "ifft", "rfft"])
155-
@pytest.mark.parametrize("dtype", ["float64", "complex128"])
173+
@pytest.mark.parametrize(
174+
"func,dtype", _cases(["fft", "ifft", "rfft"], ["float64", "complex128"])
175+
)
156176
@pytest.mark.parametrize("axis", range(len(_SHAPE_4D)))
157177
@pytest.mark.parametrize("order", _ORDERS)
158178
def test_fft_axis_4d(func, dtype, axis, order):
159179
"""A rank-4 array has two interior axes, so the per-vector fallback in the
160180
C backend is exercised twice within one sweep.
161181
"""
162-
if func == "rfft" and dtype != "float64":
163-
pytest.skip("rfft takes real input")
164182
x = _relayout(_make(_SHAPE_4D, dtype), order)
165183
got = getattr(mkl_fft, func)(x, axis=axis)
166184
want = getattr(np.fft, func)(x, axis=axis)
167185
_check(got, want, dtype)
168186

169187

170-
@pytest.mark.parametrize("func", ["rfft", "irfft"])
171-
@pytest.mark.parametrize("dtype", _DTYPES)
188+
@pytest.mark.parametrize("func,dtype", _cases(["rfft", "irfft"]))
172189
@pytest.mark.parametrize("axis", range(len(_SHAPE_3D)))
173190
@pytest.mark.parametrize("order", _ORDERS)
174191
def test_rfft_axis_3d(func, dtype, axis, order):
175-
if func == "rfft" and dtype not in _REAL_DTYPES:
176-
pytest.skip("rfft takes real input")
177192
x = _relayout(_make(_SHAPE_3D, dtype), order)
178193
got = getattr(mkl_fft, func)(x, axis=axis)
179194
want = getattr(np.fft, func)(x, axis=axis)
@@ -226,7 +241,7 @@ def test_ortho_roundtrip_is_identity(shape, axes):
226241

227242
@pytest.mark.parametrize("func", ["fftn", "ifftn"])
228243
@pytest.mark.parametrize("dtype", ["float64", "complex128"])
229-
@pytest.mark.parametrize("axes", [(0,), (1,), (2,), (1, 2), None])
244+
@pytest.mark.parametrize("axes", _AXES_3D)
230245
@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"])
231246
def test_fftn_axes_subset_norm(func, dtype, axes, norm):
232247
x = _make(_SHAPE_3D, dtype)
@@ -235,37 +250,24 @@ def test_fftn_axes_subset_norm(func, dtype, axes, norm):
235250
_check(got, want, dtype)
236251

237252

238-
@pytest.mark.parametrize("func", ["rfftn", "irfftn"])
239-
@pytest.mark.parametrize("dtype", ["float64", "complex128"])
240-
@pytest.mark.parametrize("axes", [(0,), (1,), (2,), (1, 2), None])
253+
@pytest.mark.parametrize(
254+
"func,dtype", _cases(["rfftn", "irfftn"], ["float64", "complex128"])
255+
)
256+
@pytest.mark.parametrize("axes", _AXES_3D)
241257
@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"])
242258
def test_rfftn_axes_subset_norm(func, dtype, axes, norm):
243259
"""Includes ``axes=None``: for c2r the scale basis is the *output* length
244260
along the last transformed axis, so a full-axes irfftn is normalized over
245261
``2 * (n - 1)`` rather than ``n``.
246262
"""
247-
if func == "rfftn" and dtype != "float64":
248-
pytest.skip("rfftn takes real input")
249263
x = _make(_SHAPE_3D, dtype)
250264
got = getattr(mkl_fft, func)(x, axes=axes, norm=norm)
251265
want = getattr(np.fft, func)(x, axes=axes, norm=norm)
252266
_check(got, want, dtype)
253267

254268

255-
@pytest.mark.parametrize("func", ["fftn", "ifftn", "fft2", "ifft2"])
256-
@pytest.mark.parametrize("dtype", ["float64", "complex128"])
257-
@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"])
258-
def test_empty_axes_is_identity(func, dtype, norm):
259-
"""``axes=()`` transforms nothing, so the scale is 1.0 under every norm and
260-
the input comes back untouched, as it does from NumPy.
261-
"""
262-
x = _make(_SHAPE_3D, dtype)
263-
got = getattr(mkl_fft, func)(x, axes=(), norm=norm)
264-
want = getattr(np.fft, func)(x, axes=(), norm=norm)
265-
_check(got, want, dtype)
266-
assert got is x, "no axes transformed, so the input should be returned"
267-
268-
269+
# c2c axes=() is covered by test_fftnd.py::test_empty_axes and
270+
# ::test_empty_axes_returns_same_object; only the r2c family is missing there.
269271
@pytest.mark.parametrize("func", ["rfftn", "irfftn"])
270272
@pytest.mark.parametrize("norm", [None, "forward", "ortho"])
271273
def test_empty_axes_r2c_raises_like_numpy(func, norm):
@@ -279,15 +281,15 @@ def test_empty_axes_r2c_raises_like_numpy(func, norm):
279281
getattr(mkl_fft, func)(x, axes=(), norm=norm)
280282

281283

282-
@pytest.mark.parametrize("func", ["fft2", "ifft2", "rfft2", "irfft2"])
283-
@pytest.mark.parametrize("dtype", ["float64", "complex128"])
284+
@pytest.mark.parametrize(
285+
"func,dtype",
286+
_cases(["fft2", "ifft2", "rfft2", "irfft2"], ["float64", "complex128"]),
287+
)
284288
@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"])
285289
def test_fft2_on_3d_norm(func, dtype, norm):
286290
"""``fft2`` on a rank-3 array transforms 2 of 3 axes, so it is a subset
287291
transform even though the caller passed no ``axes``.
288292
"""
289-
if func == "rfft2" and dtype != "float64":
290-
pytest.skip("rfft2 takes real input")
291293
x = _make(_SHAPE_3D, dtype)
292294
got = getattr(mkl_fft, func)(x, norm=norm)
293295
want = getattr(np.fft, func)(x, norm=norm)
@@ -420,3 +422,51 @@ def test_rfftn_shape_arg_with_norm(axes, s, norm):
420422
got = mkl_fft.rfftn(x, s=s, axes=axes, norm=norm)
421423
want = np.fft.rfftn(x, s=s, axes=axes, norm=norm)
422424
_check(got, want, "float64")
425+
426+
427+
_R2C_AXES_S = [
428+
((2,), (20,)),
429+
((2,), (10,)),
430+
((1, 2), (5, 20)),
431+
((0, 2), (10, 24)),
432+
((-1,), (20,)),
433+
((-2, -1), (5, 20)),
434+
]
435+
436+
_C2C_AXES_S = [
437+
((0,), (16,)),
438+
((0,), (4,)),
439+
((1, 2), (10, 20)),
440+
((0, 1, 2), (16, 4, 20)),
441+
]
442+
443+
444+
@pytest.mark.parametrize("func", ["rfftn", "irfftn"])
445+
@pytest.mark.parametrize("axes,s", _R2C_AXES_S)
446+
@pytest.mark.parametrize("norm", ["forward", "ortho"])
447+
def test_r2c_axes_subset_s_and_norm(func, axes, s, norm):
448+
dtype = "float64" if func == "rfftn" else "complex128"
449+
x = _make(_SHAPE_3D, dtype)
450+
got = getattr(mkl_fft, func)(x, s=s, axes=axes, norm=norm)
451+
want = getattr(np.fft, func)(x, s=s, axes=axes, norm=norm)
452+
_check(got, want, dtype)
453+
454+
455+
@pytest.mark.parametrize("func", ["fftn", "ifftn"])
456+
@pytest.mark.parametrize("axes,s", _C2C_AXES_S)
457+
@pytest.mark.parametrize("norm", ["forward", "ortho"])
458+
def test_out_with_s_and_norm(func, axes, s, norm):
459+
x = _make(_SHAPE_3D, "complex128")
460+
_check_out(func, x, "complex128", s=s, axes=axes, norm=norm)
461+
462+
463+
@pytest.mark.parametrize("func", ["fft2", "ifft2", "rfft2", "irfft2"])
464+
@pytest.mark.parametrize("shape", [(16, 18), _SHAPE_3D])
465+
@pytest.mark.parametrize("s", [(8, 9), (32, 36), (8, 36)])
466+
@pytest.mark.parametrize("norm", ["forward", "ortho"])
467+
def test_fft2_family_with_s_and_norm(func, shape, s, norm):
468+
dtype = "float64" if func == "rfft2" else "complex128"
469+
x = _make(shape, dtype)
470+
got = getattr(mkl_fft, func)(x, s=s, norm=norm)
471+
want = getattr(np.fft, func)(x, s=s, norm=norm)
472+
_check(got, want, dtype)

0 commit comments

Comments
 (0)