diff --git a/ci/scripts/test.sh b/ci/scripts/test.sh index be5da252b2517262dcc0c171717b0c82520f6ddb..5c3f1f9e59579f1a87984aec1f7fe2a9703e969e 100755 --- a/ci/scripts/test.sh +++ b/ci/scripts/test.sh @@ -52,6 +52,7 @@ python -c 'import hysop; print hysop' python "$HYSOP_DIR/core/arrays/tests/test_array.py" $HYSOP_DIR/fields/tests/test_cartesian.sh +python "$HYSOP_DIR/numerics/tests/test_fft.py" python "$HYSOP_DIR/operator/tests/test_analytic.py" python "$HYSOP_DIR/operator/tests/test_transpose.py" python "$HYSOP_DIR/operator/tests/test_derivative.py" diff --git a/cmake/hysop_tests.cmake b/cmake/hysop_tests.cmake index 8288fa8f9ffadb19afdfd797808e88f0c5de8f6e..9978521f9a1799f500eae7b2636052a09edd1609 100755 --- a/cmake/hysop_tests.cmake +++ b/cmake/hysop_tests.cmake @@ -52,7 +52,7 @@ endmacro() set(testDir ${HYSOP_BUILD_PYTHONPATH}) # === Set the list of all directories which may contain tests === -set(py_src_dirs core/arrays fields operator) +set(py_src_dirs core/arrays numerics fields operator) # === Create the files list from all directories in py_src_dirs === diff --git a/hysop/numerics/fft/gpyfft_fft.py b/hysop/numerics/fft/gpyfft_fft.py index e454eb68af3316fd256a1c2b738394d436485a53..b6aef6cf4b8c34b06f735e4e402634e0679063bd 100644 --- a/hysop/numerics/fft/gpyfft_fft.py +++ b/hysop/numerics/fft/gpyfft_fft.py @@ -4,7 +4,7 @@ FFT iterface for fast Fourier Transforms using CLFFT backend. :class:`~hysop.numerics.GpyFFTPlan` """ -import warnings, struct +import warnings, struct, primefac import numpy as np from abc import abstractmethod @@ -250,6 +250,7 @@ class GpyFFTPlan(FFTPlanI): (out_array.strides[t_axes_in[0]] == out_array.dtype.itemsize)), \ 'Inplace real transforms need stride 1 for first transform axis.' + self.check_transform_shape(t_shape) plan = GFFT.create_plan(self.context, t_shape[::-1]) plan.inplace = t_inplace plan.strides_in = t_strides_in[::-1] @@ -342,7 +343,7 @@ t_inplace, t_shape, t_precision, layout_in, layout_out, plan.scale_forward, plan.scale_backward, self.pre_callback_src, self.post_callback_src) print msg - + def set_callbacks(self, plan, axes, N, in_array, out_array, fake_input, fake_output, layout_in, layout_out, **kwds): @@ -380,6 +381,24 @@ self.pre_callback_src, self.post_callback_src) if (post_src is not None): plan.set_callback('post_callback', post_src, 'post', user_data=user_data) return (in_data, out_data) + + @classmethod + def check_transform_shape(self, shape): + """Check that clFFT can handle the logical transform size.""" + valid_factors = {2,3,5,7,11,13} + for Ni in shape: + factors = tuple( primefac.primefac(int(Ni)) ) + invalid_factors = set(factors) - valid_factors + if invalid_factors: + factorization = ' * '.join('{}^{}'.format(factor, factors.count(factor)) + for factor in set(factors)) + candidates = ', '.join(str(vf) for vf in valid_factors) + msg ='\nInvalid transform shape {} for clFFT:' + msg+='\n {} = {}' + msg+='\nOnly {} prime factors are available.' + msg+='\n' + msg=msg.format(shape, Ni, factorization, candidates) + raise ValueError(msg) @classmethod def calculate_transform_strides(cls, taxes, array): diff --git a/hysop/numerics/tests/test_fft.py b/hysop/numerics/tests/test_fft.py index 0ea1a2bce9224b307350fd8b6e60bb0f85b5a20d..17e81d67a3e8709acb2074ca58b4fd869985b9df 100644 --- a/hysop/numerics/tests/test_fft.py +++ b/hysop/numerics/tests/test_fft.py @@ -123,7 +123,7 @@ class TestFFT(object): print '\n FORWARD R2C: real to hermitian complex transform' for (shape, cshape, rshape, N, Nc, Nr) in self.iter_shapes(): print ' RFFT shape={:9s} '.format(str(shape)+':'), - Href = np.random.rand(N).astype(dtype).reshape(shape) + Href = np.random.rand(*shape).astype(dtype).reshape(shape) results = {} for (kind, implementations) in self.implementations.iteritems(): for (name, impl) in implementations.iteritems(): @@ -444,7 +444,7 @@ class TestFFT(object): print '\n R2C-C2R transform' for (shape, cshape, rshape, N, Nc, Nr) in self.iter_shapes(): print ' X - IRFFT(RFFT(X)) shape={:9s} '.format(str(shape)+':'), - Href = np.random.rand(N).astype(dtype).reshape(shape) + Href = np.random.rand(*shape).astype(dtype).reshape(shape) results = {} for (kind, implementations) in self.implementations.iteritems(): for (name, impl) in implementations.iteritems(): @@ -467,8 +467,8 @@ class TestFFT(object): results[name] = np.max(np.abs(Href-H0)) check_distances(results) - return print '\n R2R-R2R transforms' + types = ['I ','II ','III','IV '] for (itype,stype) in enumerate(types, 1): print '\n DCT-{}: real to real discrete cosine transform {}'.format( @@ -476,64 +476,81 @@ class TestFFT(object): ttype = 'COS{}'.format(itype) for (shape, cshape, rshape, N, Nc, Nr) in self.iter_shapes(): print ' X - I{}({}(X)) shape={:9s} '.format(ttype, ttype, str(shape)+':'), - Href = np.random.rand(N).astype(dtype).reshape(shape) - H0 = np.empty(shape=shape, dtype=dtype) - H1 = np.empty(shape=shape, dtype=dtype) - H0, H1, Href = self.simd_align(H0, H1, Href) + if (itype==1): # real size is 2*(N-1) + shape = mk_shape(shape, -1, shape[-1] + 1) + Href = np.random.rand(*shape).astype(dtype).reshape(shape) results = {} - for (name, impl) in self.implementations.iteritems(): - iitype = [1,3,2,4][itype-1] - if dtype not in impl.supported_ftypes: - continue - if itype not in impl.supported_cosine_transforms: - continue - if iitype not in impl.supported_cosine_transforms: - continue - forward = impl.dct(a=H0, out=H1, type=itype) - backward = impl.idct(a=H1, out=H0, type=itype) - H0[...] = Href - forward.execute() - backward.execute() - results[name] = np.max(np.abs(Href-H0)) + for (kind, implementations) in self.implementations.iteritems(): + for (name, impl) in implementations.iteritems(): + iitype = [1,3,2,4][itype-1] + if dtype not in impl.supported_ftypes: + continue + if itype not in impl.supported_cosine_transforms: + continue + if iitype not in impl.supported_cosine_transforms: + continue + D0 = impl.backend.empty(shape=shape, dtype=dtype) + D1 = impl.backend.empty(shape=shape, dtype=dtype) + forward = impl.dct(a=D0, out=D1, type=itype).setup() + backward = impl.idct(a=D1, out=D0, type=itype).setup() + assert forward.input_array is D0 + assert forward.output_array is D1 + assert backward.input_array is D1 + assert backward.output_array is D0 + D0[...] = Href + D1[...] = np.nan + forward.execute() + D0[...] = np.nan + backward.execute() + H0 = D0.get() + results[name] = np.max(np.abs(Href-H0)) check_distances(results) for (itype,stype) in enumerate(types, 1): - print '\n DST-{}: real to real discrete sine transform {}'.format( + print '\n DST-{}: real to real discrete sinine transform {}'.format( stype.strip(), itype) ttype = 'SIN{}'.format(itype) for (shape, cshape, rshape, N, Nc, Nr) in self.iter_shapes(): print ' X - I{}({}(X)) shape={:9s} '.format(ttype, ttype, str(shape)+':'), - Href = np.random.rand(N).astype(dtype).reshape(shape) - H0 = np.empty(shape=shape, dtype=dtype) - H1 = np.empty(shape=shape, dtype=dtype) - H0, H1, Href = self.simd_align(H0, H1, Href) + if (itype==1): # real size is 2*(N+1) + shape = mk_shape(shape, -1, shape[-1] - 1) + Href = np.random.rand(*shape).astype(dtype).reshape(shape) results = {} - for (name, impl) in self.implementations.iteritems(): - iitype = [1,3,2,4][itype-1] - if dtype not in impl.supported_ftypes: - continue - if itype not in impl.supported_sine_transforms: - continue - if iitype not in impl.supported_sine_transforms: - continue - forward = impl.dst(a=H0, out=H1, type=itype) - backward = impl.idst(a=H1, out=H0, type=itype) - H0[...] = Href - forward.execute() - backward.execute() - results[name] = np.max(np.abs(Href-H0)) + for (kind, implementations) in self.implementations.iteritems(): + for (name, impl) in implementations.iteritems(): + iitype = [1,3,2,4][itype-1] + if dtype not in impl.supported_ftypes: + continue + if itype not in impl.supported_sine_transforms: + continue + if iitype not in impl.supported_sine_transforms: + continue + D0 = impl.backend.empty(shape=shape, dtype=dtype) + D1 = impl.backend.empty(shape=shape, dtype=dtype) + forward = impl.dst(a=D0, out=D1, type=itype).setup() + backward = impl.idst(a=D1, out=D0, type=itype).setup() + assert forward.input_array is D0 + assert forward.output_array is D1 + assert backward.input_array is D1 + assert backward.output_array is D0 + D0[...] = Href + D1[...] = np.nan + forward.execute() + D0[...] = np.nan + backward.execute() + H0 = D0.get() + results[name] = np.max(np.abs(Href-H0)) check_distances(results) def iter_shapes(self): minj=(2,2) - maxj=(3,3) - # maxj=(6,6) + maxj=(5,5) msg = ('EVEN', 'ODD') for i in xrange(2): base = 2+i print ' '+msg[i] for j1 in xrange(minj[i],maxj[i]): - shape = (2,4,3,base**j1,) + shape = (3,2,base**j1,) cshape = list(shape) cshape[-1] = cshape[-1]//2 + 1 cshape = tuple(cshape) @@ -566,15 +583,6 @@ class TestFFT(object): ss += (s,) failed = (Eeps>report_eps) if failed: - print - print 'r0={}, r1={}'.format(r0, r1) - print - print results[r0] - print - print results[r1] - print - import sys - sys.exit(1) shape=results[r0].shape failures.setdefault(tag, []).append((r0, r1, shape, Einf, Eeps)) @@ -585,12 +593,6 @@ class TestFFT(object): msg='Some implementations did not agree on the result !' raise RuntimeError(msg) - def simd_align(self, *arrays): - aligned_arrays = () - for array in arrays: - aligned_arrays += (pyfftw.byte_align(array),) - return aligned_arrays - def report_failures(self, failures): print print '== TEST FAILURES REPORT ==' @@ -635,9 +637,9 @@ class TestFFT(object): # not testing np.longdouble because only one implementation supports it # ie. we cannot compare results between different implementations failures = {} - for dtype in (np.float64,): + for dtype in (np.float32, np.float64,): self._test_forward_backward_1d(dtype=dtype) - # self._test_1d(dtype=dtype, failures=failures.setdefault(dtype.__name__, {})) + self._test_1d(dtype=dtype, failures=failures.setdefault(dtype.__name__, {})) self.report_failures(failures) if __name__ == '__main__':