Skip to content

Commit 9ad7238

Browse files
author
Vahid Tavanashad
committed
make array Hermetian before calling c2r
1 parent 9792685 commit 9ad7238

File tree

5 files changed

+91
-74
lines changed

5 files changed

+91
-74
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ This release achieves 100% compliance with Python Array API specification (revis
2929
* Removed `einsum_call` keyword from `dpnp.einsum_path` signature [#2421](https://github.com/IntelPython/dpnp/pull/2421)
3030
* Changed `"max dimensions"` to `None` in array API capabilities [#2432](https://github.com/IntelPython/dpnp/pull/2432)
3131
* Updated kernel header `i0.hpp` to expose `cyl_bessel_i0` function depending on build target [#2440](https://github.com/IntelPython/dpnp/pull/2440)
32+
* Updated FFT module to make input array Hermitian before calling complex-to-real FFT [#2444](https://github.com/IntelPython/dpnp/pull/2444)
3233

3334
### Fixed
3435

dpnp/fft/dpnp_iface_fft.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -640,11 +640,11 @@ def ifft(a, n=None, axis=-1, norm=None, out=None):
640640
:obj:`dpnp.fft.fft`, i.e.,
641641
642642
* ``a[0]`` should contain the zero frequency term,
643-
* ``a[1:n//2]`` should contain the positive-frequency terms,
643+
* ``a[1:(n+1)//2]`` should contain the positive-frequency terms,
644644
* ``a[n//2 + 1:]`` should contain the negative-frequency terms, in
645645
increasing order starting from the most negative frequency.
646646
647-
For an even number of input points, ``A[n//2]`` represents the sum of
647+
For an even number of input points, ``a[n//2]`` represents the sum of
648648
the values at the positive and negative Nyquist frequencies, as the two
649649
are aliased together.
650650
@@ -1062,7 +1062,7 @@ def irfft(a, n=None, axis=-1, norm=None, out=None):
10621062
10631063
This function computes the inverse of the one-dimensional *n*-point
10641064
discrete Fourier Transform of real input computed by :obj:`dpnp.fft.rfft`.
1065-
In other words, ``irfft(rfft(a), len(a)) == a`` to within numerical
1065+
In other words, ``irfft(rfft(a), n=len(a)) == a`` to within numerical
10661066
accuracy. (See Notes below for why ``len(a)`` is necessary here.)
10671067
10681068
The input is expected to be in the form returned by :obj:`dpnp.fft.rfft`,
@@ -1265,9 +1265,9 @@ def irfftn(a, s=None, axes=None, norm=None, out=None):
12651265
This function computes the inverse of the *N*-dimensional discrete Fourier
12661266
Transform for real input over any number of axes in an *M*-dimensional
12671267
array by means of the Fast Fourier Transform (FFT). In other words,
1268-
``irfftn(rfftn(a), a.shape) == a`` to within numerical accuracy. (The
1269-
``a.shape`` is necessary like ``len(a)`` is for :obj:`dpnp.fft.irfft`,
1270-
and for the same reason.)
1268+
``irfftn(rfftn(a), s=a.shape, axes=range(a.ndim)) == a`` to within
1269+
numerical accuracy. (The ``a.shape`` is necessary like ``len(a)`` is for
1270+
:obj:`dpnp.fft.irfft`, and for the same reason.)
12711271
12721272
The input should be ordered in the same way as is returned by
12731273
:obj:`dpnp.fft.rfftn`, i.e. as for :obj:`dpnp.fft.irfft` for the final

dpnp/fft/dpnp_utils_fft.py

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -285,25 +285,30 @@ def _copy_array(x, complex_input):
285285
dtype = map_dtype_to_device(dpnp.float64, x.sycl_device)
286286

287287
if copy_flag:
288-
x_copy = dpnp.empty_like(x, dtype=dtype, order="C")
289-
290-
exec_q = x.sycl_queue
291-
_manager = dpu.SequentialOrderManager[exec_q]
292-
dep_evs = _manager.submitted_events
293-
294-
ht_copy_ev, copy_ev = ti._copy_usm_ndarray_into_usm_ndarray(
295-
src=dpnp.get_usm_ndarray(x),
296-
dst=x_copy.get_array(),
297-
sycl_queue=exec_q,
298-
depends=dep_evs,
299-
)
300-
_manager.add_event_pair(ht_copy_ev, copy_ev)
301-
x = x_copy
288+
x = _copy_kernel(x, dtype)
302289

303290
# if copying is done, FFT can be in-place (copy_flag = in_place flag)
304291
return x, copy_flag
305292

306293

294+
def _copy_kernel(x, dtype):
295+
x_copy = dpnp.empty_like(x, dtype=dtype, order="C")
296+
297+
exec_q = x.sycl_queue
298+
_manager = dpu.SequentialOrderManager[exec_q]
299+
dep_evs = _manager.submitted_events
300+
301+
ht_copy_ev, copy_ev = ti._copy_usm_ndarray_into_usm_ndarray(
302+
src=dpnp.get_usm_ndarray(x),
303+
dst=x_copy.get_array(),
304+
sycl_queue=exec_q,
305+
depends=dep_evs,
306+
)
307+
_manager.add_event_pair(ht_copy_ev, copy_ev)
308+
309+
return x_copy
310+
311+
307312
def _extract_axes_chunk(a, s, chunk_size=3):
308313
"""
309314
Classify the first input into a list of lists with each list containing
@@ -433,6 +438,41 @@ def _fft(a, norm, out, forward, in_place, c2c, axes, batch_fft=True):
433438
return result
434439

435440

441+
def _make_array_hermitian(a, n, copy_needed):
442+
"""
443+
For `dpnp.fft.irfft`, the input array should be Hermitian. If it is not,
444+
the behavior is undefined. This function makes necessary changes to make
445+
sure the given array is Hermitian.
446+
447+
It is assumed that this function is called after `_cook_nd_args` and so
448+
`n` is always ``None``. It is also assumed that it is called after
449+
`_truncate_or_pad`, so the array has enough length.
450+
"""
451+
452+
length_is_even = n % 2 == 0
453+
hermitian = dpnp.all(a[0].imag == 0)
454+
assert n is not None
455+
if length_is_even:
456+
# Nyquist mode (n//2+1 mode) is n//2-th element
457+
f_ny = n // 2
458+
assert a.shape[0] > f_ny
459+
hermitian = hermitian and dpnp.all(a[f_ny].imag == 0)
460+
else:
461+
# No Nyquist mode
462+
pass
463+
464+
if not hermitian:
465+
if copy_needed:
466+
a = _copy_kernel(a, a.dtype)
467+
468+
a[0].imag = 0
469+
if length_is_even:
470+
f_ny = n // 2
471+
a[f_ny].imag = 0
472+
473+
return a
474+
475+
436476
def _scale_result(res, a_shape, norm, forward, index):
437477
"""Scale the result of the FFT according to `norm`."""
438478
if res.dtype in [dpnp.float32, dpnp.complex64]:
@@ -559,6 +599,7 @@ def dpnp_fft(a, forward, real, n=None, axis=-1, norm=None, out=None):
559599
"""Calculates 1-D FFT of the input array along axis"""
560600

561601
_check_norm(norm)
602+
a_orig = a
562603
a_ndim = a.ndim
563604
if a_ndim == 0:
564605
raise ValueError("Input array must be at least 1D")
@@ -591,6 +632,14 @@ def dpnp_fft(a, forward, real, n=None, axis=-1, norm=None, out=None):
591632
if a.size == 0:
592633
return dpnp.get_result_array(a, out=out, casting="same_kind")
593634

635+
if c2r:
636+
# input array should be Hermitian for c2r FFT
637+
a = dpnp.moveaxis(a, axis, 0)
638+
a = _make_array_hermitian(
639+
a, a.shape[0], dpnp.are_same_logical_tensors(a, a_orig)
640+
)
641+
a = dpnp.moveaxis(a, 0, axis)
642+
594643
return _fft(
595644
a,
596645
norm=norm,
@@ -607,6 +656,7 @@ def dpnp_fft(a, forward, real, n=None, axis=-1, norm=None, out=None):
607656
def dpnp_fftn(a, forward, real, s=None, axes=None, norm=None, out=None):
608657
"""Calculates N-D FFT of the input array along axes"""
609658

659+
a_orig = a
610660
if isinstance(axes, Sequence) and len(axes) == 0:
611661
if real:
612662
raise IndexError("Empty axes.")
@@ -636,8 +686,14 @@ def dpnp_fftn(a, forward, real, s=None, axes=None, norm=None, out=None):
636686
len_axes = len(axes)
637687
if len_axes == 1:
638688
a = _truncate_or_pad(a, (s[-1],), (axes[-1],))
689+
if c2r:
690+
a = dpnp.moveaxis(a, axes[-1], 0)
691+
a = _make_array_hermitian(
692+
a, a.shape[0], dpnp.are_same_logical_tensors(a, a_orig)
693+
)
694+
a = dpnp.moveaxis(a, 0, axes[-1])
639695
return _fft(
640-
a, norm, out, forward, in_place and c2c, c2c, axes[0], a.ndim != 1
696+
a, norm, out, forward, in_place and c2c, c2c, axes[-1], a.ndim != 1
641697
)
642698

643699
if r2c:
@@ -686,6 +742,12 @@ def dpnp_fftn(a, forward, real, s=None, axes=None, norm=None, out=None):
686742
batch_fft=a.ndim != len_axes - 1,
687743
)
688744
a = _truncate_or_pad(a, (s[-1],), (axes[-1],))
745+
if c2r:
746+
a = dpnp.moveaxis(a, axes[-1], 0)
747+
a = _make_array_hermitian(
748+
a, a.shape[0], dpnp.are_same_logical_tensors(a, a_orig)
749+
)
750+
a = dpnp.moveaxis(a, 0, axes[-1])
689751
return _fft(
690752
a, norm, out, forward, in_place and c2c, c2c, axes[-1], a.ndim != 1
691753
)

dpnp/tests/test_fft.py

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,6 @@
2020
from .third_party.cupy import testing
2121

2222

23-
def _make_array_Hermitian(a, n):
24-
"""
25-
For `dpnp.fft.irfft` and `dpnp.fft.hfft`, the input array should be Hermitian.
26-
If it is not, the behavior is undefined. This function makes necessary changes
27-
to make sure the given array is Hermitian.
28-
"""
29-
30-
a[0] = a[0].real
31-
if n is None:
32-
# last element is Nyquist mode
33-
a[-1] = a[-1].real
34-
elif n % 2 == 0:
35-
# Nyquist mode (n//2+1 mode) is n//2-th element
36-
f_ny = n // 2
37-
if a.shape[0] > f_ny:
38-
a[f_ny] = a[f_ny].real
39-
a[f_ny + 1 :] = 0 # no data needed after Nyquist mode
40-
else:
41-
# No Nyquist mode
42-
f_ny = n // 2
43-
if a.shape[0] > f_ny:
44-
a[f_ny + 1 :] = 0 # no data needed for the second half
45-
46-
return a
47-
48-
4923
class TestFft:
5024
@pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True))
5125
@pytest.mark.parametrize("n", [None, 5, 20])
@@ -577,13 +551,14 @@ def test_basic(self, func, dtype, axes):
577551

578552

579553
class TestHfft:
580-
@pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True))
554+
# TODO: include boolean dtype when mkl_fft-gh-180 is merged
555+
@pytest.mark.parametrize(
556+
"dtype", get_all_dtypes(no_none=True, no_bool=True)
557+
)
581558
@pytest.mark.parametrize("n", [None, 5, 18])
582559
@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"])
583560
def test_basic(self, dtype, n, norm):
584561
a = generate_random_numpy_array(11, dtype)
585-
if numpy.issubdtype(dtype, numpy.complexfloating):
586-
a = _make_array_Hermitian(a, n)
587562
ia = dpnp.array(a)
588563

589564
result = dpnp.fft.hfft(ia, n=n, norm=norm)
@@ -626,8 +601,6 @@ class TestIrfft:
626601
@pytest.mark.parametrize("norm", [None, "backward", "forward", "ortho"])
627602
def test_basic(self, dtype, n, norm):
628603
a = generate_random_numpy_array(11)
629-
if numpy.issubdtype(dtype, numpy.complexfloating):
630-
a = _make_array_Hermitian(a, n)
631604
ia = dpnp.array(a)
632605

633606
result = dpnp.fft.irfft(ia, n=n, norm=norm)
@@ -644,10 +617,6 @@ def test_basic(self, dtype, n, norm):
644617
def test_2d_array(self, dtype, n, axis, norm, order):
645618
a = generate_random_numpy_array((3, 4), dtype=dtype, order=order)
646619
ia = dpnp.array(a)
647-
# For dpnp, each 1-D array of input should be Hermitian
648-
ia = dpnp.moveaxis(ia, axis, 0)
649-
ia = _make_array_Hermitian(ia, n)
650-
ia = dpnp.moveaxis(ia, 0, axis)
651620

652621
result = dpnp.fft.irfft(ia, n=n, axis=axis, norm=norm)
653622
expected = numpy.fft.irfft(a, n=n, axis=axis, norm=norm)
@@ -661,10 +630,6 @@ def test_2d_array(self, dtype, n, axis, norm, order):
661630
def test_3d_array(self, dtype, n, axis, norm, order):
662631
a = generate_random_numpy_array((4, 5, 6), dtype, order)
663632
ia = dpnp.array(a)
664-
# For dpnp, each 1-D array of input should be Hermitian
665-
ia = dpnp.moveaxis(ia, axis, 0)
666-
ia = _make_array_Hermitian(ia, n)
667-
ia = dpnp.moveaxis(ia, 0, axis)
668633

669634
result = dpnp.fft.irfft(ia, n=n, axis=axis, norm=norm)
670635
expected = numpy.fft.irfft(a, n=n, axis=axis, norm=norm)
@@ -674,7 +639,6 @@ def test_3d_array(self, dtype, n, axis, norm, order):
674639
def test_usm_ndarray(self, n):
675640
a = generate_random_numpy_array(11, dtype=numpy.complex64)
676641
a_usm = dpt.asarray(a)
677-
a_usm = _make_array_Hermitian(a_usm, n)
678642

679643
expected = numpy.fft.irfft(a, n=n)
680644
out = dpt.empty(expected.shape, dtype=a_usm.real.dtype)
@@ -688,7 +652,6 @@ def test_usm_ndarray(self, n):
688652
def test_out(self, dtype, n, norm):
689653
a = generate_random_numpy_array(11, dtype=dtype)
690654
ia = dpnp.array(a)
691-
ia = _make_array_Hermitian(ia, n)
692655

693656
expected = numpy.fft.irfft(a, n=n, norm=norm)
694657
out = dpnp.empty(expected.shape, dtype=a.real.dtype)
@@ -704,9 +667,6 @@ def test_out(self, dtype, n, norm):
704667
def test_2d_array_out(self, dtype, n, axis, norm, order):
705668
a = generate_random_numpy_array((3, 4), dtype=dtype, order=order)
706669
ia = dpnp.array(a)
707-
ia = dpnp.moveaxis(ia, axis, 0)
708-
ia = _make_array_Hermitian(ia, n)
709-
ia = dpnp.moveaxis(ia, 0, axis)
710670

711671
expected = numpy.fft.irfft(a, n=n, axis=axis, norm=norm)
712672
out = dpnp.empty(expected.shape, dtype=expected.dtype)
@@ -994,5 +954,7 @@ def test_1d_array(self):
994954

995955
result = dpnp.fft.irfftn(ia)
996956
expected = numpy.fft.irfftn(a)
997-
flag = True if numpy_version() < "2.0.0" else False
957+
# TODO: change to the commented line when mkl_fft-gh-180 is merged
958+
flag = True
959+
# flag = True if numpy_version() < "2.0.0" else False
998960
assert_dtype_allclose(result, expected, check_only_type_kind=flag)

dpnp/tests/third_party/cupy/fft_tests/test_fft.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,10 +1047,6 @@ def test_rfft2(self, xp, dtype, order, enable_nd):
10471047
)
10481048
def test_irfft2(self, xp, dtype, order, enable_nd):
10491049
# assert config.enable_nd_planning == enable_nd
1050-
1051-
if self.s is None and self.axes in [None, (-2, -1)]:
1052-
pytest.skip("Input is not Hermitian Symmetric")
1053-
10541050
a = testing.shaped_random(self.shape, xp, dtype)
10551051
if order == "F":
10561052
a = xp.asfortranarray(a)
@@ -1137,10 +1133,6 @@ def test_rfftn(self, xp, dtype, order, enable_nd):
11371133
)
11381134
def test_irfftn(self, xp, dtype, order, enable_nd):
11391135
# assert config.enable_nd_planning == enable_nd
1140-
1141-
if self.s is None and self.axes in [None, (-2, -1)]:
1142-
pytest.skip("Input is not Hermitian Symmetric")
1143-
11441136
a = testing.shaped_random(self.shape, xp, dtype)
11451137
if order == "F":
11461138
a = xp.asfortranarray(a)

0 commit comments

Comments
 (0)