Repository: scipy/scipy
Pull request: FIX: Allow non-native byte order inputs to scipy.fft
#### Reference issue
Fixes https://github.com/scipy/scipy/issues/10422#issuecomment-587040183

#### What does this implement/fix?
`_pocketfft` transforms now accept inputs in non-native byte-order by copying the data to native byte-order first.

Problem statement:
BUG: scipy.fft does not support multiprocessing
`scipy.fft` interacts badly with `joblib`, specifically when using the "processes" method. @peterbell10 tested and reports that it also fails for `multiprocessing.Pool` directly. But here was my MWE with joblib:
```Python
>>> import numpy as np
>>> from joblib import Parallel, delayed
>>> from scipy.fft import rfft
>>> [rfft(np.zeros(1)) for _ in range(2)]  # works
>>> Parallel(n_jobs=2, prefer='threads')(delayed(rfft)(np.zeros(1)) for _ in range(2))  # works
>>> Parallel(n_jobs=2, prefer='processes')(delayed(rfft)(np.zeros(1)) for _ in range(2))  # fails
joblib.externals.loky.process_executor._RemoteTraceback: 
"""
Traceback (most recent call last):
  File "/home/larsoner/python/joblib/joblib/externals/loky/process_executor.py", line 418, in _process_worker
    r = call_item()
  File "/home/larsoner/python/joblib/joblib/externals/loky/process_executor.py", line 272, in __call__
    return self.fn(*self.args, **self.kwargs)
  File "/home/larsoner/python/joblib/joblib/_parallel_backends.py", line 567, in __call__
    return self.func(*args, **kwargs)
  File "/home/larsoner/python/joblib/joblib/parallel.py", line 225, in __call__
    for func, args, kwargs in self.items]
  File "/home/larsoner/python/joblib/joblib/parallel.py", line 225, in <listcomp>
    for func, args, kwargs in self.items]
  File "/home/larsoner/python/scipy/scipy/fft/_basic.py", line 298, in rfft
    return _pocketfft.rfft(x, n, axis, norm, overwrite_x)
  File "/home/larsoner/python/scipy/scipy/fft/_pocketfft/basic.py", line 134, in rfft
    return pfft.r2c(tmp, (axis,), True, norm, None, _default_workers)
RuntimeError: unsupported data type
"""

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/larsoner/python/joblib/joblib/parallel.py", line 934, in __call__
    self.retrieve()
  File "/home/larsoner/python/joblib/joblib/parallel.py", line 833, in retrieve
    self._output.extend(job.get(timeout=self.timeout))
  File "/home/larsoner/python/joblib/joblib/_parallel_backends.py", line 521, in wrap_future_result
    return future.result(timeout=timeout)
  File "/usr/lib/python3.7/concurrent/futures/_base.py", line 432, in result
    return self.__get_result()
  File "/usr/lib/python3.7/concurrent/futures/_base.py", line 384, in __get_result
    raise self._exception
RuntimeError: unsupported data type
```
Modifying the SciPy code to print the dtype and `__class__` of at least `tmp` shows `np.float64` and `np.ndarray` as expected, so I'm not sure what unsupported datatype we are getting.

Hints from issue discussion:
Does rfft use openmp?
It's unrelated. The root cause is comparing dtypes using `is` which breaks after `fork`. I have a fix locally, PR soon.
Some aspects of this bug still seem to exist in 1.4.1: I now get this error in fftconvolve unless I specifically cast my data (original dtype: ">f4") as a numpy.float32 dtype, which wasn't necessary before.

Commit message: FIX: Allow non-native byte order inputs to scipy.fft

Patch to review:
diff --git a/scipy/fft/_pocketfft/helper.py b/scipy/fft/_pocketfft/helper.py
index 89b32ab88379..b75514e0c151 100644
--- a/scipy/fft/_pocketfft/helper.py
+++ b/scipy/fft/_pocketfft/helper.py
@@ -91,8 +91,11 @@ def _asfarray(x):
     elif x.dtype.kind not in 'fc':
         return np.asarray(x, np.float64)
 
+    # Require native byte order
+    dtype = x.dtype.newbyteorder('=')
     # Always align input
-    return np.array(x, copy=not x.flags['ALIGNED'])
+    copy = not x.flags['ALIGNED']
+    return np.array(x, dtype=dtype, copy=copy)
 
 def _datacopied(arr, original):
     """
diff --git a/scipy/fft/_pocketfft/tests/test_basic.py b/scipy/fft/_pocketfft/tests/test_basic.py
index 35a39a7b84a2..c222ebffd382 100644
--- a/scipy/fft/_pocketfft/tests/test_basic.py
+++ b/scipy/fft/_pocketfft/tests/test_basic.py
@@ -8,7 +8,8 @@
 import pytest
 from pytest import raises as assert_raises
 from scipy.fft._pocketfft import (ifft, fft, fftn, ifftn,
-                                  rfft, irfft, rfftn, irfftn, fft2)
+                                  rfft, irfft, rfftn, irfftn, fft2,
+                                  hfft, ihfft, hfftn, ihfftn)
 
 from numpy import (arange, add, array, asarray, zeros, dot, exp, pi,
                    swapaxes, double, cdouble)
@@ -45,6 +46,10 @@ def _assert_close_in_norm(x, y, rtol, size, rdt):
 def random(size):
     return rand(*size)
 
+def swap_byteorder(arr):
+    """Returns the same array with swapped byteorder"""
+    dtype = arr.dtype.newbyteorder('S')
+    return arr.astype(dtype)
 
 def get_mat(n):
     data = arange(n)
@@ -1001,3 +1006,18 @@ def test_invalid_norm(func):
     with assert_raises(ValueError,
                        match='Invalid norm value o, should be None or "ortho"'):
         func(x, norm='o')
+
+
+@pytest.mark.parametrize('func', [fft, ifft, fftn, ifftn,
+                                   irfft, irfftn, hfft, hfftn])
+def test_swapped_byte_order_complex(func):
+    rng = np.random.RandomState(1234)
+    x = rng.rand(10) + 1j * rng.rand(10)
+    assert_allclose(func(swap_byteorder(x)), func(x))
+
+
+@pytest.mark.parametrize('func', [ihfft, ihfftn, rfft, rfftn])
+def test_swapped_byte_order_complex(func):
+    rng = np.random.RandomState(1234)
+    x = rng.rand(10)
+    assert_allclose(func(swap_byteorder(x)), func(x))
diff --git a/scipy/fft/_pocketfft/tests/test_real_transforms.py b/scipy/fft/_pocketfft/tests/test_real_transforms.py
index 41c29a3f67a6..54d745b99dc5 100644
--- a/scipy/fft/_pocketfft/tests/test_real_transforms.py
+++ b/scipy/fft/_pocketfft/tests/test_real_transforms.py
@@ -479,3 +479,12 @@ def test_shape_is_none_with_axes(self, fforward, finverse, axes):
         tmp = fforward(self.data, s=None, axes=axes, norm='ortho')
         tmp = finverse(tmp, s=None, axes=axes, norm='ortho')
         assert_array_almost_equal(self.data, tmp, decimal=self.dec)
+
+
+@pytest.mark.parametrize('func', [dct, dctn, idct, idctn,
+                                  dst, dstn, idst, idstn])
+def test_swapped_byte_order(func):
+    rng = np.random.RandomState(1234)
+    x = rng.rand(10)
+    swapped_dt = x.dtype.newbyteorder('S')
+    assert_allclose(func(x.astype(swapped_dt)), func(x))


Code review comments:
diff_hunk: @@ -1001,3 +1006,18 @@ def test_invalid_norm(func):
     with assert_raises(ValueError,
                        match='Invalid norm value o, should be None or "ortho"'):
         func(x, norm='o')
+
+
+@pytest.mark.parametrize('func', [fft, ifft, fftn, ifftn,
+                                   irfft, irfftn, hfft, hfftn])
+def test_swapped_byte_order_complex(func):
+    rng = np.random.RandomState(1234)
+    x = rng.rand(10) + 1j * rng.rand(10)
+    assert_allclose(func(swap_byteorder(x)), func(x))
+
+
+@pytest.mark.parametrize('func', [ihfft, ihfftn, rfft, rfftn])
+def test_swapped_byte_order_complex(func):
path: scipy/fft/_pocketfft/tests/test_basic.py
text: @user1:
```suggestion
def test_swapped_byte_order_real(func):
```