|
| 1 | +"""Cross-library equivalence checks for axis and axes dispatch. |
| 2 | +
|
| 3 | +``third_party/scipy/test_basic.py::test_fft_with_order`` already checks that |
| 4 | +mkl_fft agrees with *itself* across C, Fortran, and non-contiguous layouts. It |
| 5 | +does not compare against an external reference, so a dispatch change that is |
| 6 | +consistently wrong in every layout passes it. |
| 7 | +
|
| 8 | +The defect recorded in ``_fft_utils._iter_complementary`` was exactly that |
| 9 | +kind: values correct, but an element placed in the other half of the output |
| 10 | +relative to NumPy. These tests therefore use ``numpy.fft`` as the reference. |
| 11 | +
|
| 12 | +Two deliberate choices: |
| 13 | +
|
| 14 | +* Every axis length differs, so an axis permutation cannot produce a |
| 15 | + correctly shaped result and hide behind a shape assertion. |
| 16 | +* Output dtype is asserted alongside values, so a dispatch change cannot |
| 17 | + silently upcast. |
| 18 | +
|
| 19 | +These cover the paths that dispatch on *which* axes are requested: full-axes |
| 20 | +transforms reach the batched N-D descriptor, strict subsets iterate the |
| 21 | +complementary axes, and 1-D transforms of rank > 2 arrays are batched only for |
| 22 | +the first and last axis. |
| 23 | +""" |
| 24 | + |
| 25 | +import itertools |
| 26 | + |
| 27 | +import numpy as np |
| 28 | +import pytest |
| 29 | +from numpy.testing import assert_allclose |
| 30 | + |
| 31 | +import mkl_fft |
| 32 | + |
| 33 | +_SHAPE_3D = (8, 7, 13) |
| 34 | +_SHAPE_4D = (4, 5, 6, 7) |
| 35 | + |
| 36 | +_DTYPES = ["float32", "float64", "complex64", "complex128"] |
| 37 | +_REAL_DTYPES = ["float32", "float64"] |
| 38 | + |
| 39 | +_ORDERS = ["C", "F", "non-contiguous"] |
| 40 | + |
| 41 | +# Relative tolerance by input precision. Single-precision transforms of |
| 42 | +# random data over these lengths stay well inside 2e-5. |
| 43 | +_TOL = { |
| 44 | + "float32": 2e-5, |
| 45 | + "complex64": 2e-5, |
| 46 | + "float64": 1e-12, |
| 47 | + "complex128": 1e-12, |
| 48 | +} |
| 49 | + |
| 50 | +# every non-empty subset of the axes of a 3-D array, plus None |
| 51 | +_AXES_3D = [ |
| 52 | + ax for n in (1, 2, 3) for ax in itertools.combinations(range(3), n) |
| 53 | +] + [None] |
| 54 | + |
| 55 | + |
| 56 | +def _make(shape, dtype, seed=42): |
| 57 | + rng = np.random.default_rng(seed) |
| 58 | + dt = np.dtype(dtype) |
| 59 | + if dt.kind == "c": |
| 60 | + x = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) |
| 61 | + else: |
| 62 | + x = rng.standard_normal(shape) |
| 63 | + return x.astype(dt) |
| 64 | + |
| 65 | + |
| 66 | +def _relayout(x, order): |
| 67 | + """Return *x* laid out as requested; data content may differ by order.""" |
| 68 | + if order == "F": |
| 69 | + return np.asfortranarray(x) |
| 70 | + if order == "non-contiguous": |
| 71 | + return x[::-1] |
| 72 | + return np.ascontiguousarray(x) |
| 73 | + |
| 74 | + |
| 75 | +def _check(got, want, dtype): |
| 76 | + assert got.dtype == want.dtype, f"dtype {got.dtype} != {want.dtype}" |
| 77 | + assert got.shape == want.shape, f"shape {got.shape} != {want.shape}" |
| 78 | + tol = _TOL[dtype] |
| 79 | + assert_allclose( |
| 80 | + got, want, rtol=tol, atol=tol * max(1.0, float(np.abs(want).max())) |
| 81 | + ) |
| 82 | + |
| 83 | + |
| 84 | +# --------------------------------------------------------------------------- |
| 85 | +# N-D complex transforms over a subset of axes |
| 86 | +# --------------------------------------------------------------------------- |
| 87 | + |
| 88 | + |
| 89 | +@pytest.mark.parametrize("func", ["fftn", "ifftn"]) |
| 90 | +@pytest.mark.parametrize("dtype", _DTYPES) |
| 91 | +@pytest.mark.parametrize("axes", _AXES_3D) |
| 92 | +@pytest.mark.parametrize("order", _ORDERS) |
| 93 | +def test_fftn_axes_subset(func, dtype, axes, order): |
| 94 | + x = _relayout(_make(_SHAPE_3D, dtype), order) |
| 95 | + got = getattr(mkl_fft, func)(x, axes=axes) |
| 96 | + want = getattr(np.fft, func)(x, axes=axes) |
| 97 | + _check(got, want, dtype) |
| 98 | + |
| 99 | + |
| 100 | +@pytest.mark.parametrize("func", ["rfftn", "irfftn"]) |
| 101 | +@pytest.mark.parametrize("dtype", _DTYPES) |
| 102 | +@pytest.mark.parametrize("axes", _AXES_3D) |
| 103 | +@pytest.mark.parametrize("order", _ORDERS) |
| 104 | +def test_rfftn_axes_subset(func, dtype, axes, order): |
| 105 | + if func == "rfftn" and dtype not in _REAL_DTYPES: |
| 106 | + pytest.skip("rfftn takes real input") |
| 107 | + x = _relayout(_make(_SHAPE_3D, dtype), order) |
| 108 | + got = getattr(mkl_fft, func)(x, axes=axes) |
| 109 | + want = getattr(np.fft, func)(x, axes=axes) |
| 110 | + _check(got, want, dtype) |
| 111 | + |
| 112 | + |
| 113 | +# --------------------------------------------------------------------------- |
| 114 | +# 1-D transforms along each axis of a higher-rank array |
| 115 | +# --------------------------------------------------------------------------- |
| 116 | + |
| 117 | + |
| 118 | +@pytest.mark.parametrize("func", ["fft", "ifft"]) |
| 119 | +@pytest.mark.parametrize("dtype", _DTYPES) |
| 120 | +@pytest.mark.parametrize("axis", range(len(_SHAPE_3D))) |
| 121 | +@pytest.mark.parametrize("order", _ORDERS) |
| 122 | +def test_fft_axis_3d(func, dtype, axis, order): |
| 123 | + x = _relayout(_make(_SHAPE_3D, dtype), order) |
| 124 | + got = getattr(mkl_fft, func)(x, axis=axis) |
| 125 | + want = getattr(np.fft, func)(x, axis=axis) |
| 126 | + _check(got, want, dtype) |
| 127 | + |
| 128 | + |
| 129 | +@pytest.mark.parametrize("func", ["fft", "ifft", "rfft"]) |
| 130 | +@pytest.mark.parametrize("dtype", ["float64", "complex128"]) |
| 131 | +@pytest.mark.parametrize("axis", range(len(_SHAPE_4D))) |
| 132 | +@pytest.mark.parametrize("order", _ORDERS) |
| 133 | +def test_fft_axis_4d(func, dtype, axis, order): |
| 134 | + """A rank-4 array has two interior axes, so the per-vector fallback in the |
| 135 | + C backend is exercised twice within one sweep. |
| 136 | + """ |
| 137 | + if func == "rfft" and dtype != "float64": |
| 138 | + pytest.skip("rfft takes real input") |
| 139 | + x = _relayout(_make(_SHAPE_4D, dtype), order) |
| 140 | + got = getattr(mkl_fft, func)(x, axis=axis) |
| 141 | + want = getattr(np.fft, func)(x, axis=axis) |
| 142 | + _check(got, want, dtype) |
| 143 | + |
| 144 | + |
| 145 | +@pytest.mark.parametrize("func", ["rfft", "irfft"]) |
| 146 | +@pytest.mark.parametrize("dtype", _DTYPES) |
| 147 | +@pytest.mark.parametrize("axis", range(len(_SHAPE_3D))) |
| 148 | +@pytest.mark.parametrize("order", _ORDERS) |
| 149 | +def test_rfft_axis_3d(func, dtype, axis, order): |
| 150 | + if func == "rfft" and dtype not in _REAL_DTYPES: |
| 151 | + pytest.skip("rfft takes real input") |
| 152 | + x = _relayout(_make(_SHAPE_3D, dtype), order) |
| 153 | + got = getattr(mkl_fft, func)(x, axis=axis) |
| 154 | + want = getattr(np.fft, func)(x, axis=axis) |
| 155 | + _check(got, want, dtype) |
| 156 | + |
| 157 | + |
| 158 | +# --------------------------------------------------------------------------- |
| 159 | +# norm interacts with the scale factor applied at dispatch time |
| 160 | +# --------------------------------------------------------------------------- |
| 161 | + |
| 162 | + |
| 163 | +@pytest.mark.parametrize("func", ["fftn", "ifftn"]) |
| 164 | +@pytest.mark.parametrize("dtype", ["float64", "complex128"]) |
| 165 | +@pytest.mark.parametrize("axes", [(0,), (1,), (2,), (1, 2), None]) |
| 166 | +@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"]) |
| 167 | +def test_fftn_axes_subset_norm(func, dtype, axes, norm): |
| 168 | + x = _make(_SHAPE_3D, dtype) |
| 169 | + got = getattr(mkl_fft, func)(x, axes=axes, norm=norm) |
| 170 | + want = getattr(np.fft, func)(x, axes=axes, norm=norm) |
| 171 | + _check(got, want, dtype) |
| 172 | + |
| 173 | + |
| 174 | +@pytest.mark.parametrize("func", ["rfftn", "irfftn"]) |
| 175 | +@pytest.mark.parametrize("dtype", ["float64", "complex128"]) |
| 176 | +@pytest.mark.parametrize("axes", [(0,), (1,), (2,), (1, 2), None]) |
| 177 | +@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"]) |
| 178 | +def test_rfftn_axes_subset_norm(func, dtype, axes, norm): |
| 179 | + """Includes ``axes=None``: for c2r the scale basis is the *output* length |
| 180 | + along the last transformed axis, so a full-axes irfftn is normalized over |
| 181 | + ``2 * (n - 1)`` rather than ``n``. |
| 182 | + """ |
| 183 | + if func == "rfftn" and dtype != "float64": |
| 184 | + pytest.skip("rfftn takes real input") |
| 185 | + x = _make(_SHAPE_3D, dtype) |
| 186 | + got = getattr(mkl_fft, func)(x, axes=axes, norm=norm) |
| 187 | + want = getattr(np.fft, func)(x, axes=axes, norm=norm) |
| 188 | + _check(got, want, dtype) |
| 189 | + |
| 190 | + |
| 191 | +@pytest.mark.parametrize("func", ["fft2", "ifft2", "rfft2", "irfft2"]) |
| 192 | +@pytest.mark.parametrize("dtype", ["float64", "complex128"]) |
| 193 | +@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"]) |
| 194 | +def test_fft2_on_3d_norm(func, dtype, norm): |
| 195 | + """``fft2`` on a rank-3 array transforms 2 of 3 axes, so it is a subset |
| 196 | + transform even though the caller passed no ``axes``. |
| 197 | + """ |
| 198 | + if func == "rfft2" and dtype != "float64": |
| 199 | + pytest.skip("rfft2 takes real input") |
| 200 | + x = _make(_SHAPE_3D, dtype) |
| 201 | + got = getattr(mkl_fft, func)(x, norm=norm) |
| 202 | + want = getattr(np.fft, func)(x, norm=norm) |
| 203 | + _check(got, want, dtype) |
| 204 | + |
| 205 | + |
| 206 | +@pytest.mark.parametrize("func", ["fft", "ifft"]) |
| 207 | +@pytest.mark.parametrize("dtype", ["float64", "complex128"]) |
| 208 | +@pytest.mark.parametrize("axis", range(len(_SHAPE_3D))) |
| 209 | +@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"]) |
| 210 | +def test_fft_axis_norm(func, dtype, axis, norm): |
| 211 | + x = _make(_SHAPE_3D, dtype) |
| 212 | + got = getattr(mkl_fft, func)(x, axis=axis, norm=norm) |
| 213 | + want = getattr(np.fft, func)(x, axis=axis, norm=norm) |
| 214 | + _check(got, want, dtype) |
| 215 | + |
| 216 | + |
| 217 | +# --------------------------------------------------------------------------- |
| 218 | +# out= must not change results on any dispatch path |
| 219 | +# --------------------------------------------------------------------------- |
| 220 | + |
| 221 | + |
| 222 | +@pytest.mark.parametrize("dtype", ["complex64", "complex128"]) |
| 223 | +@pytest.mark.parametrize("axes", _AXES_3D) |
| 224 | +def test_fftn_axes_subset_out(dtype, axes): |
| 225 | + x = _make(_SHAPE_3D, dtype) |
| 226 | + want = np.fft.fftn(x, axes=axes) |
| 227 | + out = np.empty(want.shape, dtype=x.dtype) |
| 228 | + got = mkl_fft.fftn(x, axes=axes, out=out) |
| 229 | + assert got is out, "out= should be returned" |
| 230 | + _check(got, want, dtype) |
0 commit comments