Skip to content

Commit 22857d2

Browse files
authored
Merge branch 'master' into change-pre-commit-clang-check
2 parents 51fd9a0 + 27583c9 commit 22857d2

8 files changed

Lines changed: 200 additions & 16 deletions

File tree

.github/workflows/build-with-clang.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
steps:
2727
- name: Cancel Previous Runs
28-
uses: styfle/cancel-workflow-action@3155a141048f8f89c06b4cdae32e7853e97536bc # 0.13.0
28+
uses: styfle/cancel-workflow-action@d07a454dad7609a92316b57b23c9ccfd4f59af66 # 0.13.1
2929
with:
3030
access_token: ${{ github.token }}
3131

.github/workflows/conda-package-cf.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434

3535
steps:
3636
- name: Cancel Previous Runs
37-
uses: styfle/cancel-workflow-action@3155a141048f8f89c06b4cdae32e7853e97536bc # 0.13.0
37+
uses: styfle/cancel-workflow-action@d07a454dad7609a92316b57b23c9ccfd4f59af66 # 0.13.1
3838
with:
3939
access_token: ${{ github.token }}
4040

@@ -98,7 +98,7 @@ jobs:
9898

9999
steps:
100100
- name: Download artifact
101-
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
101+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
102102
with:
103103
name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_ver }}
104104

@@ -172,7 +172,7 @@ jobs:
172172

173173
steps:
174174
- name: Cancel Previous Runs
175-
uses: styfle/cancel-workflow-action@3155a141048f8f89c06b4cdae32e7853e97536bc # 0.13.0
175+
uses: styfle/cancel-workflow-action@d07a454dad7609a92316b57b23c9ccfd4f59af66 # 0.13.1
176176
with:
177177
access_token: ${{ github.token }}
178178

@@ -242,7 +242,7 @@ jobs:
242242

243243
steps:
244244
- name: Download artifact
245-
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
245+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
246246
with:
247247
name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_ver }}
248248

.github/workflows/conda-package.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
2424
steps:
2525
- name: Cancel Previous Runs
26-
uses: styfle/cancel-workflow-action@3155a141048f8f89c06b4cdae32e7853e97536bc # 0.13.0
26+
uses: styfle/cancel-workflow-action@d07a454dad7609a92316b57b23c9ccfd4f59af66 # 0.13.1
2727
with:
2828
access_token: ${{ github.token }}
2929

@@ -86,7 +86,7 @@ jobs:
8686

8787
steps:
8888
- name: Download artifact
89-
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
89+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
9090
with:
9191
name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }}
9292

@@ -161,7 +161,7 @@ jobs:
161161
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
162162
steps:
163163
- name: Cancel Previous Runs
164-
uses: styfle/cancel-workflow-action@3155a141048f8f89c06b4cdae32e7853e97536bc # 0.13.0
164+
uses: styfle/cancel-workflow-action@d07a454dad7609a92316b57b23c9ccfd4f59af66 # 0.13.1
165165
with:
166166
access_token: ${{ github.token }}
167167

@@ -232,7 +232,7 @@ jobs:
232232

233233
steps:
234234
- name: Download artifact
235-
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
235+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
236236
with:
237237
name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }}
238238

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ repos:
4646
- tomli
4747

4848
- repo: https://github.com/psf/black
49-
rev: 26.3.0
49+
rev: 26.3.1
5050
hooks:
5151
- id: black
5252
exclude: "_vendored/conv_template.py"
@@ -112,7 +112,7 @@ repos:
112112
- id: shellcheck
113113

114114
- repo: https://github.com/gitleaks/gitleaks
115-
rev: v8.30.0
115+
rev: v8.30.1
116116
hooks:
117117
- id: gitleaks
118118

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
### Removed
1616
* Dropped support for Python 3.9 [gh-243](https://github.com/IntelPython/mkl_fft/pull/243)
1717

18+
### Fixed
19+
* Fix `TypeError` exception raised with empty axes [gh-288](https://github.com/IntelPython/mkl_fft/pull/288)
20+
* To return input array unchanged when `axes=()` (ignore `out` parameter) [gh-293](https://github.com/IntelPython/mkl_fft/pull/293)
21+
1822
## [2.1.2] - 2025-12-02
1923

2024
### Added

mkl_fft/_fft_utils.py

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,67 @@ def _init_nd_shape_and_axes(x, shape, axes):
209209

210210

211211
def _iter_complementary(x, axes, func, kwargs, result):
212+
"""
213+
Apply FFT function by iterating over complementary axes.
214+
215+
This function applies an FFT operation to slices of the input array
216+
by iterating over all axes that are NOT in the `axes` parameter
217+
(the complementary axes). For each position in the complementary axes,
218+
it applies the FFT function to a slice along the specified axes.
219+
220+
Parameters
221+
----------
222+
x : ndarray
223+
Input array.
224+
axes : int, sequence of ints, or None
225+
Axes along which to perform the FFT operation. The function iterates
226+
over the complementary axes (axes not in this parameter). If ``None``,
227+
performs direct N-D FFT without iteration.
228+
Default: None
229+
func : callable
230+
FFT function to apply to each slice. Should accept array input and
231+
return transformed output.
232+
kwargs : dict
233+
Additional keyword arguments to pass to `func`.
234+
result : ndarray
235+
Pre-allocated output array where results are stored.
236+
237+
Returns
238+
-------
239+
ndarray
240+
The transformed array (same as `result`).
241+
242+
Notes
243+
-----
244+
For complex input, the function uses in-place operations with the `out`
245+
parameter passed for better performance. For real input, `np.copyto` is
246+
used instead to avoid element ordering issues that can occur with the
247+
`out` parameter in certain FFT operations.
248+
249+
Examples
250+
--------
251+
Consider an input array with shape (3, 4, 5) and performing FFT
252+
along axis 2 only:
253+
254+
>>> x = np.random.random((3, 4, 5))
255+
>>> result = np.empty((3, 4, 5), dtype=np.complex128)
256+
>>> _iter_complementary(
257+
... x, axes=(2,), func=_direct_fftnd,
258+
... kwargs={'direction': 1, 'fsc': 1.0}, result=result
259+
... )
260+
261+
The function will iterate over axes 0 and 1 (complementary axes)
262+
and apply `_direct_fftnd` to each 1-D slice along axis 2:
263+
264+
- Iteration 0: func(x[0, 0, :]) -> result[0, 0, :]
265+
- Iteration 1: func(x[0, 1, :]) -> result[0, 1, :]
266+
- ...
267+
- Iteration 11: func(x[2, 3, :]) -> result[2, 3, :]
268+
269+
Total: 3 * 4 = 12 FFT operations on arrays of shape (5,).
270+
271+
"""
272+
212273
if axes is None:
213274
# s and axes are None, direct N-D FFT
214275
return func(x, **kwargs, out=result)
@@ -233,8 +294,10 @@ def _iter_complementary(x, axes, func, kwargs, result):
233294
m_ind = _flat_to_multi(ind, sub_shape)
234295
for k1, k2 in zip(dual_ind, m_ind):
235296
sl[k1] = k2
297+
tsl = tuple(sl)
298+
236299
if np.issubdtype(x.dtype, np.complexfloating):
237-
func(x[tuple(sl)], **kwargs, out=result[tuple(sl)])
300+
func(x[tsl], **kwargs, out=result[tsl])
238301
else:
239302
# For c2c FFT, if the input is real, half of the output is the
240303
# complex conjugate of the other half. Instead of upcasting the
@@ -247,7 +310,7 @@ def _iter_complementary(x, axes, func, kwargs, result):
247310
# array appeared in the second half of the NumPy output array,
248311
# while the equivalent element in the NumPy array was the conjugate
249312
# of the mkl_fft output array.
250-
np.copyto(result[tuple(sl)], func(x[tuple(sl)], **kwargs))
313+
np.copyto(result[tsl], func(x[tsl], **kwargs))
251314

252315
return result
253316

@@ -260,7 +323,49 @@ def _iter_fftnd(
260323
direction=+1,
261324
scale_function=lambda ind: 1.0,
262325
):
263-
a = np.asarray(a)
326+
"""
327+
Perform N-D FFT as a series of 1-D FFTs along specified axes.
328+
329+
This function implements N-D FFT by applying 1-D FFT iteratively along each
330+
axis. The axes are processed in reverse order to end with the first axis
331+
given.
332+
333+
Parameters
334+
----------
335+
a : ndarray
336+
Input array.
337+
s : sequence of ints, optional
338+
Shape of the FFT output along each axis in `axes`. If not provided, the
339+
shape is inferred from the input array.
340+
Default: ``None``
341+
axes : sequence of ints, optional
342+
Axes along which to compute the FFT. If not provided, all axes are used.
343+
Default: ``None``
344+
out : ndarray, optional
345+
Output array to store the result. Used for in-place operations when
346+
possible.
347+
Default: ``None``
348+
direction : int, optional
349+
FFT direction: ``+1`` for forward FFT, ``-1`` for inverse FFT.
350+
Default: ``+1``
351+
scale_function : callable, optional
352+
Function that takes iteration index and returns the scaling factor for
353+
that step. Used to apply normalization at specific iteration steps.
354+
Default: ``lambda ind: 1.0``
355+
356+
Returns
357+
-------
358+
ndarray
359+
The transformed array.
360+
361+
Notes
362+
-----
363+
The function optimizes memory usage by performing in-place calculations
364+
when possible. In-place operations are used everywhere except when the
365+
array size changes after the first FFT along an axis.
366+
367+
"""
368+
264369
s, axes = _init_nd_shape_and_axes(a, s, axes)
265370

266371
# Combine the two, but in reverse, to end with the first axis given.
@@ -412,8 +517,17 @@ def _c2c_fftnd_impl(
412517
out=out,
413518
)
414519
else:
520+
x = np.asarray(x)
521+
522+
# Fast path: FFT over no axes is a complete identity operation.
523+
# Returns the input unchanged (same object, no copy), preserving
524+
# dtype and avoiding any FFT computation. The out parameter is
525+
# ignored to match NumPy behavior.
526+
_, xa = _cook_nd_args(x, s, axes)
527+
if len(xa) == 0:
528+
return x
529+
415530
if _complementary and x.dtype in valid_dtypes:
416-
x = np.asarray(x)
417531
if out is None:
418532
res = np.empty_like(x, dtype=_output_dtype(x.dtype))
419533
else:

mkl_fft/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.2.0dev1"
1+
__version__ = "2.2.0dev5"

mkl_fft/tests/test_fftnd.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,69 @@ def test_out_strided(axes, func):
317317
expected = getattr(np.fft, func)(x, axes=axes, out=out)
318318

319319
assert_allclose(result, expected, strict=True)
320+
321+
322+
@pytest.mark.parametrize(
323+
"dtype", [np.float32, np.float64, np.complex64, np.complex128]
324+
)
325+
@pytest.mark.parametrize("shape", [(3, 4), (5,), (2, 3, 4), (10, 20)])
326+
@pytest.mark.parametrize("norm", [None, "ortho", "forward", "backward"])
327+
@pytest.mark.parametrize("func", ["fftn", "ifftn", "fft2", "ifft2"])
328+
def test_empty_axes(dtype, shape, norm, func):
329+
if np.issubdtype(dtype, np.complexfloating):
330+
x = rnd.random(shape).astype(dtype) + 1j * rnd.random(shape).astype(
331+
dtype
332+
)
333+
else:
334+
x = rnd.random(shape).astype(dtype)
335+
336+
# Test fftn with axes=()
337+
result = getattr(mkl_fft, func)(x, axes=(), norm=norm)
338+
expected = getattr(np.fft, func)(x, axes=(), norm=norm)
339+
340+
rtol, atol = _get_rtol_atol(result)
341+
assert_allclose(result, expected, rtol=rtol, atol=atol, strict=True)
342+
343+
344+
@pytest.mark.parametrize(
345+
"dtype", [np.float32, np.float64, np.complex64, np.complex128]
346+
)
347+
@pytest.mark.parametrize("func", ["fftn", "ifftn", "fft2", "ifft2"])
348+
def test_empty_axes_with_out(dtype, func):
349+
if np.issubdtype(dtype, np.complexfloating):
350+
x = rnd.random((3, 4)).astype(dtype) + 1j * rnd.random((3, 4)).astype(
351+
dtype
352+
)
353+
else:
354+
x = rnd.random((3, 4)).astype(dtype)
355+
356+
# NumPy ignores out parameter when axes=() and returns input
357+
out = np.empty_like(x, dtype=dtype)
358+
result = getattr(mkl_fft, func)(x, axes=(), out=out)
359+
expected = getattr(np.fft, func)(x, axes=(), out=out)
360+
361+
# Result should be the input array (out parameter ignored)
362+
assert result is x, f"{func} with axes=() should return input (ignore out)"
363+
assert expected is x, "NumPy should also return input"
364+
assert result is expected
365+
366+
367+
@pytest.mark.parametrize(
368+
"dtype", [np.float32, np.float64, np.complex64, np.complex128]
369+
)
370+
@pytest.mark.parametrize("func", ["fftn", "ifftn", "fft2", "ifft2"])
371+
def test_empty_axes_returns_same_object(dtype, func):
372+
if np.issubdtype(dtype, np.complexfloating):
373+
x = rnd.random((3, 4)).astype(dtype) + 1j * rnd.random((3, 4)).astype(
374+
dtype
375+
)
376+
else:
377+
x = rnd.random((3, 4)).astype(dtype)
378+
379+
# Without out parameter, should return the same object
380+
result = getattr(mkl_fft, func)(x, axes=())
381+
382+
# Verify it's the exact same object (identity check)
383+
assert (
384+
result is x
385+
), f"{func} with axes=() should return the same object, not a copy"

0 commit comments

Comments
 (0)