|
| 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 | + |
1 | 26 | """Cross-library equivalence checks for axis and axes dispatch. |
2 | 27 |
|
3 | 28 | ``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): |
188 | 213 | _check(got, want, dtype) |
189 | 214 |
|
190 | 215 |
|
| 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 | + |
191 | 243 | @pytest.mark.parametrize("func", ["fft2", "ifft2", "rfft2", "irfft2"]) |
192 | 244 | @pytest.mark.parametrize("dtype", ["float64", "complex128"]) |
193 | 245 | @pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"]) |
@@ -228,3 +280,104 @@ def test_fftn_axes_subset_out(dtype, axes): |
228 | 280 | got = mkl_fft.fftn(x, axes=axes, out=out) |
229 | 281 | assert got is out, "out= should be returned" |
230 | 282 | _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