Skip to content

Commit bb74339

Browse files
committed
fix: review
1 parent 80a97ff commit bb74339

3 files changed

Lines changed: 161 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
### Changed
1212

1313
### Fixed
14-
* Fixed `norm="forward"` and `norm="ortho"` scaling in `mkl_fft.fftn`, `ifftn`, `rfftn`, `irfftn` and the `fft2`/`ifft2`/`rfft2`/`irfft2` family when only a subset of the input axes is transformed and `s` is not given. The scale factor was computed over the full array shape instead of over the transformed axes, over-normalizing the result by the product of the untransformed axis lengths (for example `fft2` on a 3-D array, or `fftn(x, axes=(0,))`). The `mkl_fft.interfaces.numpy_fft` and `mkl_fft.interfaces.scipy_fft` wrappers were unaffected, as they resolve `s` before delegating
15-
* Fixed `norm="forward"` and `norm="ortho"` scaling in `mkl_fft.irfftn` and `irfft2`, which normalized over the input length `n` along the last transformed axis rather than the complex-to-real output length `2 * (n - 1)`. This applied even when every axis was transformed
14+
* 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)
15+
* 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)
1616
* 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)
1717
* 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)
1818

mkl_fft/_fft_utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,18 @@ def _compute_nd_scale_shape(x, s, axes, norm=None, invreal=False):
103103
if axes is None:
104104
ss = list(x.shape)
105105
last = len(ss) - 1
106+
elif len(axes) == 0:
107+
# transforming no axes is an identity, so the scale is 1.0;
108+
# np.prod(()) is 1, which gives that for every norm
109+
return ()
106110
else:
107111
ss = [x.shape[ai] for ai in axes]
108112
last = axes[-1]
109113
if invreal:
110114
ss[-1] = 2 * (x.shape[last] - 1)
111115
except (IndexError, TypeError):
112-
# invalid or empty axes; leave the scale alone and let the
113-
# transform itself raise
116+
# invalid axes; leave the scale alone and let the transform itself
117+
# raise, so the error matches what NumPy reports
114118
return s
115119
return tuple(ss)
116120

mkl_fft/tests/test_dispatch_equivalence.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
# Copyright (c) 2026, Intel Corporation
2+
#
3+
# Redistribution and use in source and binary forms, with or without
4+
# modification, are permitted provided that the following conditions are met:
5+
#
6+
# * Redistributions of source code must retain the above copyright notice,
7+
# this list of conditions and the following disclaimer.
8+
# * Redistributions in binary form must reproduce the above copyright
9+
# notice, this list of conditions and the following disclaimer in the
10+
# documentation and/or other materials provided with the distribution.
11+
# * Neither the name of Intel Corporation nor the names of its contributors
12+
# may be used to endorse or promote products derived from this software
13+
# without specific prior written permission.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
19+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
126
"""Cross-library equivalence checks for axis and axes dispatch.
227
328
``third_party/scipy/test_basic.py::test_fft_with_order`` already checks that
@@ -188,6 +213,33 @@ def test_rfftn_axes_subset_norm(func, dtype, axes, norm):
188213
_check(got, want, dtype)
189214

190215

216+
@pytest.mark.parametrize("func", ["fftn", "ifftn", "fft2", "ifft2"])
217+
@pytest.mark.parametrize("dtype", ["float64", "complex128"])
218+
@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"])
219+
def test_empty_axes_is_identity(func, dtype, norm):
220+
"""``axes=()`` transforms nothing, so the scale is 1.0 under every norm and
221+
the input comes back untouched, as it does from NumPy.
222+
"""
223+
x = _make(_SHAPE_3D, dtype)
224+
got = getattr(mkl_fft, func)(x, axes=(), norm=norm)
225+
want = getattr(np.fft, func)(x, axes=(), norm=norm)
226+
_check(got, want, dtype)
227+
assert got is x, "no axes transformed, so the input should be returned"
228+
229+
230+
@pytest.mark.parametrize("func", ["rfftn", "irfftn"])
231+
@pytest.mark.parametrize("norm", [None, "forward", "ortho"])
232+
def test_empty_axes_r2c_raises_like_numpy(func, norm):
233+
"""With no axes there is no last transformed axis to hold the half
234+
spectrum, so both libraries raise; check the type agrees.
235+
"""
236+
x = _make(_SHAPE_3D, "float64")
237+
with pytest.raises(IndexError):
238+
getattr(np.fft, func)(x, axes=(), norm=norm)
239+
with pytest.raises(IndexError):
240+
getattr(mkl_fft, func)(x, axes=(), norm=norm)
241+
242+
191243
@pytest.mark.parametrize("func", ["fft2", "ifft2", "rfft2", "irfft2"])
192244
@pytest.mark.parametrize("dtype", ["float64", "complex128"])
193245
@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"])
@@ -228,3 +280,104 @@ def test_fftn_axes_subset_out(dtype, axes):
228280
got = mkl_fft.fftn(x, axes=axes, out=out)
229281
assert got is out, "out= should be returned"
230282
_check(got, want, dtype)
283+
284+
285+
def _check_out(func, x, dtype, **kwargs):
286+
"""Run *func* with an ``out`` array shaped and typed from the reference."""
287+
want = getattr(np.fft, func)(x, **kwargs)
288+
out = np.empty(want.shape, dtype=want.dtype)
289+
got = getattr(mkl_fft, func)(x, out=out, **kwargs)
290+
assert got is out, "out= should be returned"
291+
_check(got, want, dtype)
292+
293+
294+
@pytest.mark.parametrize("func", ["fftn", "ifftn"])
295+
@pytest.mark.parametrize("dtype", ["complex64", "complex128"])
296+
@pytest.mark.parametrize("axes", [(0,), (2,), (1, 2), None])
297+
@pytest.mark.parametrize("norm", ["forward", "ortho"])
298+
def test_c2c_out_with_norm(func, dtype, axes, norm):
299+
"""The scale is applied while the result is written into ``out``, which is
300+
the path this fix changes; ``ifftn`` shares it but was never exercised.
301+
"""
302+
_check_out(func, _make(_SHAPE_3D, dtype), dtype, axes=axes, norm=norm)
303+
304+
305+
@pytest.mark.parametrize("dtype", ["float32", "float64"])
306+
@pytest.mark.parametrize("axes", [(0,), (2,), (1, 2), None])
307+
@pytest.mark.parametrize("norm", [None, "forward", "ortho"])
308+
def test_rfftn_out_with_norm(dtype, axes, norm):
309+
"""r2c: ``out`` is complex with the last transformed axis reduced to
310+
``n // 2 + 1``, a different allocation from the c2c case.
311+
"""
312+
_check_out("rfftn", _make(_SHAPE_3D, dtype), dtype, axes=axes, norm=norm)
313+
314+
315+
@pytest.mark.parametrize("dtype", ["complex64", "complex128"])
316+
@pytest.mark.parametrize("axes", [(2,), (1, 2), None])
317+
@pytest.mark.parametrize("norm", [None, "forward", "ortho"])
318+
def test_irfftn_out_with_norm(dtype, axes, norm):
319+
"""c2r: ``out`` is real with the last transformed axis expanded to
320+
``2 * (n - 1)`` -- the length the invreal branch of the scale helper
321+
computes, so this ties the two together.
322+
"""
323+
_check_out("irfftn", _make(_SHAPE_3D, dtype), dtype, axes=axes, norm=norm)
324+
325+
326+
# ---------------------------------------------------------------------------
327+
# s= combined with a scaled norm
328+
#
329+
# The scale helper deliberately returns early when s is given, leaving
330+
# _compute_fwd_scale to normalize over prod(s). These lock that branch down.
331+
# axes is always passed explicitly: NumPy deprecated giving s without axes.
332+
# ---------------------------------------------------------------------------
333+
334+
335+
@pytest.mark.parametrize("func", ["fftn", "ifftn"])
336+
@pytest.mark.parametrize(
337+
"axes,s",
338+
[
339+
((0,), (16,)), # pad one axis
340+
((0,), (4,)), # truncate one axis
341+
((1, 2), (10, 20)), # pad two
342+
((1, 2), (4, 6)), # truncate two
343+
((0, 1, 2), (16, 4, 20)), # pad and truncate together
344+
],
345+
)
346+
@pytest.mark.parametrize("norm", ["forward", "ortho"])
347+
def test_c2c_shape_arg_with_norm(func, axes, s, norm):
348+
"""The scale must come from ``prod(s)`` -- the padded or truncated length --
349+
not from the original axis lengths.
350+
"""
351+
x = _make(_SHAPE_3D, "complex128")
352+
got = getattr(mkl_fft, func)(x, s=s, axes=axes, norm=norm)
353+
want = getattr(np.fft, func)(x, s=s, axes=axes, norm=norm)
354+
_check(got, want, "complex128")
355+
356+
357+
@pytest.mark.parametrize("s", [(8, 7, 20), (8, 7, 10), (8, 7, 24)])
358+
@pytest.mark.parametrize("norm", ["forward", "ortho"])
359+
def test_irfftn_shape_arg_with_norm(s, norm):
360+
"""With ``s`` given, the invreal doubling must *not* be applied: the scale
361+
normalizes over ``s[-1]``, not ``2 * (x.shape[-1] - 1)``.
362+
363+
``s=(8, 7, 24)`` is deliberately ``2 * (13 - 1)``, so a regression that
364+
ignores ``s`` and falls back to the input-derived basis would still pass
365+
that one case -- 20 and 10 are what catch it. A regression that instead
366+
doubles ``s[-1]`` itself is caught by all three.
367+
"""
368+
x = _make(_SHAPE_3D, "complex128")
369+
got = mkl_fft.irfftn(x, s=s, axes=(0, 1, 2), norm=norm)
370+
want = np.fft.irfftn(x, s=s, axes=(0, 1, 2), norm=norm)
371+
_check(got, want, "complex128")
372+
373+
374+
@pytest.mark.parametrize(
375+
"axes,s", [((1,), (10,)), ((1,), (4,)), ((1, 2), (10, 20))]
376+
)
377+
@pytest.mark.parametrize("norm", ["forward", "ortho"])
378+
def test_rfftn_shape_arg_with_norm(axes, s, norm):
379+
"""Locks the r2c ``s``-given path together with scaling."""
380+
x = _make(_SHAPE_3D, "float64")
381+
got = mkl_fft.rfftn(x, s=s, axes=axes, norm=norm)
382+
want = np.fft.rfftn(x, s=s, axes=axes, norm=norm)
383+
_check(got, want, "float64")

0 commit comments

Comments
 (0)