diff --git a/examples/bubble/periodic_jet_levelset.py b/examples/bubble/periodic_jet_levelset.py index bbb1cfe1dba86b8c9d188148cd03045fc2d5061f..1f9cdbf530a17c49cdc94dbd419d81db0e82e8e0 100644 --- a/examples/bubble/periodic_jet_levelset.py +++ b/examples/bubble/periodic_jet_levelset.py @@ -204,7 +204,7 @@ def compute(args): implementation=impl, **extra_op_kwds) ### Adaptive timestep operator - adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True, max_dt=1e-2, + adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True, max_dt=1e-4, name='merge_dt', pretty_name='dt', ) dt_cfl = adapt_dt.push_cfl_criteria(cfl=args.cfl, Finf=min_max_U.Finf, equivalent_CFL=True, diff --git a/hysop/numerics/fft/fft.py b/hysop/numerics/fft/fft.py index 798d37af6fe671be6c7e27195668756bb069cb8a..8e0fa1762a1dbf63bbde00db8b306e8a9ab83dc2 100644 --- a/hysop/numerics/fft/fft.py +++ b/hysop/numerics/fft/fft.py @@ -181,7 +181,7 @@ class FFTI(object): @abstractmethod - def irfft(self, a, out, axis=-1, build_plan=False, **kwds): + def irfft(self, a, out, n=None, axis=-1, build_plan=False, **kwds): """ Compute the one-dimensional hermitian complex to real discrete Fourier Transform scaled by 1/N. @@ -195,24 +195,50 @@ class FFTI(object): Last axis should match forward transform size used: 1) out.shape[axis] = 2*(a.shape[axis]-1) 2) out.shape[axis] = 2*(a.shape[axis]-1) + 1 + n: int, optional + Length of the transformed axis of the output. + ie: n should be in [2*(a.shape[axis]-1), 2*(a.shape[axis]-1)+1] axis: int, optional Axis over witch to compute the transform. Defaults to last axis. build_plan: bool, optional Build and return a IRFFT plan instead of applying the transform. + Notes + ----- + To get an odd number of output points, n or out must be specified. + Returns ------- - (shape0, shape1, dtype) of the output array determined from the input array. + (shape, dtype) of the output array determined from the input array, out and n. """ assert a.dtype in self.supported_ctypes cshape = a.shape rtype = complex_to_float_dtype(a.dtype) - rshape = list(a.shape) - rshape[axis] = 2*(cshape[axis]-1) + + rshape_even, rshape_odd = list(a.shape), list(a.shape) + rshape_even[axis] = 2*(cshape[axis]-1) + rshape_odd[axis] = 2*(cshape[axis]-1) + 1 + if (out is not None): assert out.dtype in self.supported_ftypes assert rtype == out.dtype - assert np.arrayequal(out.shape, rshape) + + ns = out.shape[axis] + if (n is not None): + assert ns==n, 'output shape mismatch with specified transformed output size.' + else: + n = ns + + if (n%2==0): + assert np.arrayequal(out.shape, rshape_even) + else: + assert np.arrayequal(out.shape, rshape_odd) + + if (n is None) or (n%2==0): + rshape = rshape_even + else: + rshape = rshape_odd + return (rshape, rtype) diff --git a/hysop/numerics/fft/fftw_fft.py b/hysop/numerics/fft/fftw_fft.py index 6fab55ceba4c0fa972fddb4992ce101dbe07d8bc..19eb3639162ae71240498314fdd1f931449e51d0 100644 --- a/hysop/numerics/fft/fftw_fft.py +++ b/hysop/numerics/fft/fftw_fft.py @@ -177,9 +177,10 @@ class FftwFFT(FFTI): plan = FftwFFTPlan(**kwds) return plan - def irfft(self, a, out=None, axis=-1, **kwds): + def irfft(self, a, out=None, n=None, axis=-1, **kwds): """Planning destroys initial arrays content.""" - (shape, dtype) = super(FftwFFT, self).irfft(a=a, out=out, axis=axis, **kwds) + (shape, dtype) = super(FftwFFT, self).irfft(a=a, out=out, axis=axis, + n=n, **kwds) out = self.allocate_output(out, shape, dtype) if self.warn_on_misalignment: self.check_alignment(a, out) diff --git a/hysop/numerics/fft/numpy_fft.py b/hysop/numerics/fft/numpy_fft.py index 6119e11a21b254e4e0e5eb95e34c1ddbd552f599..01918220a2213ef795817516fcc15405bd0c20c9 100644 --- a/hysop/numerics/fft/numpy_fft.py +++ b/hysop/numerics/fft/numpy_fft.py @@ -41,8 +41,8 @@ class NumpyFFTPlan(FFTPlanI): def __call__(self, out=None, **kwds): fn_kwds = self.kwds.copy() fn_kwds.update(kwds) - if (self.fn is _FFT.irfft) and (out is not None): - fn_kwds.setdefault('n', out.shape[fn_kwds['axis']]) + if (self.fn is _FFT.irfft): + assert 'n' in fn_kwds out = first_not_None(out, self.out) res = self.fn(**fn_kwds) @@ -92,9 +92,9 @@ class NumpyFFT(FFTI): **kwds) return plan - def irfft(self, a, out=None, axis=-1, **kwds): - super(NumpyFFT, self).irfft(a=a, out=out, axis=axis, **kwds) + def irfft(self, a, out=None, n=None, axis=-1, **kwds): + (rshape, _) = super(NumpyFFT, self).irfft(a=a, out=out, n=n, axis=axis, **kwds) plan = NumpyFFTPlan(fn=_FFT.irfft, a=a, out=out, axis=axis, output_dtype=lambda x: complex_to_float_dtype(x), - **kwds) + n=rshape[axis], **kwds) return plan diff --git a/hysop/numerics/fft/scipy_fft.py b/hysop/numerics/fft/scipy_fft.py new file mode 100644 index 0000000000000000000000000000000000000000..2c1e977a2b49408aac04eb5bc404bc82451dae94 --- /dev/null +++ b/hysop/numerics/fft/scipy_fft.py @@ -0,0 +1,121 @@ +""" +FFT iterface for fast Fourier Transforms using scipy fftpack backend. +:class:`~hysop.numerics.ScipyFFT` +:class:`~hysop.numerics.ScipyFFTPlan` +""" + +import numpy as np +import scipy as sp +from scipy import fftpack as _FFT + +from hysop.tools.types import first_not_None +from hysop.numerics.fft.fft import FFTPlanI, FFTI, complex_to_float_dtype, float_to_complex_dtype + + +class ScipyFFTPlan(FFTPlanI): + """ + Wrap a scipy fftpack call (scipy.fftpack does not offer real planning capabilities). + """ + + def __init__(self, fn, out, + output_dtype=lambda dtype: dtype, + **kwds): + super(ScipyFFTPlan, self).__init__() + assert callable(output_dtype) + self.fn = fn + self.out = out + self.kwds = kwds.copy() + + def execute(self): + return self.__call__() + + def __call__(self, out=None, **kwds): + fn = self.fn + fn_kwds = self.kwds.copy() + fn_kwds.update(kwds) + if (fn is _FFT.irfft): + assert 'n' in fn_kwds + + in_ = fn_kwds['x'] + + if fn is _FFT.irfft: + axis = fn_kwds['axis'] + assert axis in (in_.ndim-1, -1) + in_ = in_.view(dtype=complex_to_float_dtype(in_.dtype)) + is_even = (fn_kwds['n']%2==0) + rshape = list(in_.shape) + rshape[-1] -= 1 + is_even + _in = np.empty(shape=rshape, dtype=in_.dtype) + _in[...,1:] = in_[...,2:in_.shape[-1]-is_even] + _in[...,0] = in_[...,0] + fn_kwds['x'] = _in + + out = first_not_None(out, self.out) + res = fn(**fn_kwds) + + if fn is _FFT.rfft: + axis = fn_kwds['axis'] + assert axis in (in_.ndim-1, -1) + is_even = (in_.shape[axis] % 2 == 0) + if (out is None): + rshape = list(res.shape) + rshape[axis] += 1 + is_even + out = np.empty(dtype=in_.dtype, shape=rshape) + else: + rtype = complex_to_float_dtype(out.dtype) + out = out.view(dtype=rtype) + ctype = float_to_complex_dtype(out.dtype) + out[..., 1:out.shape[-1]-is_even] = res + out[..., 0] = out[..., 1] + out[..., 1] = 0.0 + if is_even: + out[..., -1] = 0.0 + out = out.view(dtype=ctype) + elif (out is not None): + out[...] = res + else: + out = res + return out + + +class ScipyFFT(FFTI): + """ + Interface to compute local to process FFT-like transforms using the scipy fftpack backend. + + Scipy fftpack backend has some disadvantages: + - creation of one or two intermediate temporary buffers at each call + - hermitian-complex data layout is different than all other fft implementations + - no planning capabilities (scipy.fftpack methods are just wrapped into fake plans) + + It has native float and double precision support unlike the numpy fft backend. + Planning won't destroy original inputs. + """ + + def __init__(self): + super(ScipyFFT, self).__init__() + self.supported_ftypes = (np.float32, np.float64,) + self.supported_ctypes = (np.complex64, np.complex128,) + + def fft(self, a, out=None, axis=-1, **kwds): + super(ScipyFFT, self).fft(a=a, out=out, axis=axis, **kwds) + plan = ScipyFFTPlan(fn=_FFT.fft, x=a, out=out, axis=axis, **kwds) + return plan + + def ifft(self, a, out=None, axis=-1, **kwds): + super(ScipyFFT, self).ifft(a=a, out=out, axis=axis, **kwds) + plan = ScipyFFTPlan(fn=_FFT.ifft, x=a, out=out, axis=axis, **kwds) + return plan + + def rfft(self, a, out=None, axis=-1, **kwds): + super(ScipyFFT, self).rfft(a=a, out=out, axis=axis, **kwds) + plan = ScipyFFTPlan(fn=_FFT.rfft, x=a, out=out, axis=axis, + output_dtype=lambda x: float_to_complex_dtype(x), + **kwds) + return plan + + def irfft(self, a, out=None, n=None, axis=-1, **kwds): + (rshape, _) = super(ScipyFFT, self).irfft(a=a, out=out, n=n, axis=axis, **kwds) + plan = ScipyFFTPlan(fn=_FFT.irfft, x=a, out=out, axis=axis, + output_dtype=lambda x: complex_to_float_dtype(x), + n=rshape[axis], **kwds) + return plan diff --git a/hysop/numerics/tests/test_fft.py b/hysop/numerics/tests/test_fft.py index 34088a72ed24b1bbdb744950c5a14d8446e06422..5f14b009a49d0178444e07e590bdbc2b181ef45b 100644 --- a/hysop/numerics/tests/test_fft.py +++ b/hysop/numerics/tests/test_fft.py @@ -16,6 +16,7 @@ from hysop.tools.numerics import float_to_complex_dtype from hysop.tools.types import check_instance, first_not_None from hysop.numerics.fft.numpy_fft import NumpyFFT +from hysop.numerics.fft.scipy_fft import ScipyFFT from hysop.numerics.fft.fftw_fft import FftwFFT @@ -23,6 +24,7 @@ class TestFFT(object): implementations = { 'numpy': NumpyFFT(), + 'scipy': ScipyFFT(), 'fftw': FftwFFT(warn_on_allocation=False) } @@ -31,9 +33,10 @@ class TestFFT(object): msg_input_modified = 'Input array has been modified for implementation {}.' msg_output_modified = 'Output array results are not consistent for implementation {}.' - max_eps = 2 + report_eps = 10 + fail_eps = 1000 - def _test_1d(self, dtype): + def _test_1d(self, dtype, failures): print print '::Testing 1D transform, precision {}::'.format(dtype.__name__) eps = np.finfo(dtype).eps @@ -41,8 +44,10 @@ class TestFFT(object): def iter_shapes(): msg = ('EVEN', 'ODD') + minj=(5,1) maxj=(15,10) - minj=(10,5) + #minj=(2,2) + #maxj=(5,5) for i in xrange(2): base = 2+i print ' '+msg[i] @@ -51,12 +56,16 @@ class TestFFT(object): cshape = list(shape) cshape[-1] = cshape[-1]//2 + 1 cshape = tuple(cshape) + rshape = list(shape) + rshape[-1] = (rshape[-1]//2) * 2 + rshape = tuple(rshape) N = np.prod(shape, dtype=np.int64) Nc = np.prod(cshape, dtype=np.int64) - yield (shape, cshape, N, Nc) + Nr = np.prod(rshape, dtype=np.int64) + yield (shape, cshape, rshape, N, Nc, Nr) print ' FORWARD C2C: complex to complex forward transform' - for (shape, cshape, N, Nc) in iter_shapes(): + for (shape, cshape, rshape, N, Nc, Nr) in iter_shapes(): print ' FFT shape={:9s} '.format(str(shape)+':'), Href = np.random.rand(2*N).astype(dtype).view(dtype=ctype).reshape(shape) H = np.empty_like(Href) @@ -74,11 +83,11 @@ class TestFFT(object): out[...] = 0 out = plan.execute() assert np.array_equal(out, refout), self.msg_output_modified.format(name) - results[name] = refout - self.check_distances(results, eps, N*self.max_eps) + results[name] = refout / N # forward normalization + self.check_distances(results, eps, self.report_eps, 'forward C2C', failures) print ' BACKWARD C2C: complex to complex backward transform' - for (shape, cshape, N, Nc) in iter_shapes(): + for (shape, cshape, rshape, N, Nc, Nr) in iter_shapes(): print ' IFFT shape={:9s} '.format(str(shape)+':'), Href = np.random.rand(2*N).astype(dtype).view(dtype=ctype).reshape(shape) H = np.empty_like(Href) @@ -97,10 +106,10 @@ class TestFFT(object): out = plan.execute() assert np.array_equal(out, refout), self.msg_output_modified.format(name) results[name] = refout - self.check_distances(results, eps, self.max_eps) + self.check_distances(results, eps, self.report_eps, 'backward C2C', failures) print ' FORWARD R2C: real to hermitian complex transform' - for (shape, cshape, N, Nc) in iter_shapes(): + for (shape, cshape, rshape, N, Nc, Nr) in iter_shapes(): print ' RFFT shape={:9s} '.format(str(shape)+':'), Href = np.random.rand(N).astype(dtype).reshape(shape) H = np.empty_like(Href) @@ -118,12 +127,12 @@ class TestFFT(object): out[...] = 0 out = plan.execute() assert np.array_equal(out, refout), self.msg_output_modified.format(name) - results[name] = refout - self.check_distances(results, eps, N*self.max_eps) + results[name] = refout / N #forward normalization + self.check_distances(results, eps, self.report_eps, 'R2C', failures) print ' BACKWARD C2R: real to hermitian complex transform' - for (shape, cshape, N, Nc) in iter_shapes(): - print ' RFFT shape={:9s} '.format(str(shape)+':'), + for (shape, cshape, rshape, N, Nc, Nr) in iter_shapes(): + print ' IRFFT shape={:9s} '.format(str(rshape)+':'), Href = np.random.rand(2*Nc).astype(dtype).view(dtype=ctype).reshape(cshape) H = np.empty_like(Href) H, Href = self.simd_align(H, Href) @@ -133,7 +142,29 @@ class TestFFT(object): plan = impl.irfft(a=H) H[...] = Href out = plan.execute() - #assert out.shape == shape, self.msg_shape.format(shape, out.shape, name) + assert out.shape == rshape, self.msg_shape.format(rshape, out.shape, name) + assert out.dtype == dtype, self.msg_dtype.format(dtype, out.dtype, name) + assert np.array_equal(Href, H), self.msg_input_modified.format(name) + refout = out.copy() + out[...] = 0 + out = plan.execute() + assert np.array_equal(out, refout), self.msg_output_modified.format(name) + results[name] = refout + self.check_distances(results, eps, self.report_eps, 'normal C2R', failures) + + print ' BACKWARD FORCED C2R: real to hermitian complex transform with specified shape' + for (shape, cshape, rshape, N, Nc, Nr) in iter_shapes(): + print ' IRFFT shape={:9s} '.format(str(shape)+':'), + Href = np.random.rand(2*Nc).astype(dtype).view(dtype=ctype).reshape(cshape) + H = np.empty_like(Href) + H, Href = self.simd_align(H, Href) + results = {} + for (name, impl) in self.implementations.iteritems(): + if dtype in impl.supported_ftypes: + plan = impl.irfft(a=H, n=shape[-1]) + H[...] = Href + out = plan.execute() + assert out.shape == shape, self.msg_shape.format(shape, out.shape, name) assert out.dtype == dtype, self.msg_dtype.format(dtype, out.dtype, name) assert np.array_equal(Href, H), self.msg_input_modified.format(name) refout = out.copy() @@ -141,24 +172,31 @@ class TestFFT(object): out = plan.execute() assert np.array_equal(out, refout), self.msg_output_modified.format(name) results[name] = refout - self.check_distances(results, eps, self.max_eps) + self.check_distances(results, eps, self.report_eps, 'forced C2R', failures) - def check_distances(self, results, eps, max_eps): + def check_distances(self, results, eps, report_eps, tag, failures): if len(results.keys())<2: impl = results.keys()[0] print 'cannot compare' return ss=() - failed = False for (r0,r1) in it.combinations(results.keys(), 2): + if not (results[r0].shape == results[r1].shape): + print + msg='Output shapes do not match.' + raise RuntimeError(msg) E = results[r1] - results[r0] Einf = np.max(np.abs(E)) Eeps = int(np.round(Einf/eps)) - failed |= (Eeps>max_eps) s='|{}-{}|={}eps'.format(r0,r1,Eeps) ss += (s,) + failed = (Eeps>report_eps) + if failed: + shape=results[r0].shape + failures.setdefault(tag, []).append((r0, r1, shape, Einf, Eeps)) + print ', '.join(ss) - if failed: + if failed and False: print msg='Some implementations did not agree on the result !' raise RuntimeError(msg) @@ -168,14 +206,54 @@ class TestFFT(object): for array in arrays: aligned_arrays += (pyfftw.byte_align(array),) return aligned_arrays + + def report_failures(self, failures): + print + print '== TEST FAILURES REPORT ==' + print ' Report error has been set to {} epsilons.'.format(self.report_eps) + print ' Test failure has been set to {} epsilons.'.format(self.fail_eps) + assert self.report_eps <= self.fail_eps + failed = False + cnt = 0 + for (dtype, fails) in failures.iteritems(): + if not fails: + continue + print '::{} precision tests'.format(dtype) + for (tag, tfails) in fails.iteritems(): + print ' |{} transform errors:'.format(tag) + for (r0,r1, shape, Einf, Eeps) in sorted(tfails, + key=lambda x: -x[-2]): + failed |= (Eeps >= self.fail_eps) + cnt+=1 + msg=' {} vs {}:\tshape {}\t->\tE={}\t({} eps)' + msg=msg.format(r0, r1, shape, Einf, Eeps) + print msg + print '===========================' + + if failed: + msg ='' + msg+='\n************* FFT TESTS FAILED ***************' + msg+='\n** One or more test exceeded failure error. **' + msg+='\n**********************************************' + msg+='\n' + print msg + raise RuntimeError + else: + msg ='' + msg+='\n*************** FFT TESTS PASSED ******************' + msg+='\n** Some tests may have exceeded reporting error. **' + msg+='\n***************************************************' + msg+='\n' + print msg - def test_1d_transforms(self): - self._test_1d(dtype=np.float32) - self._test_1d(dtype=np.float64) - #self._test_1d(dtype=np.longdouble) def perform_tests(self): - self.test_1d_transforms() + # not testing np.longdouble because only one implementation support it + # ie. we cannot compare results + failures = {} + for dtype in (np.float32, np.float64,): + self._test_1d(dtype=dtype, failures=failures.setdefault(dtype.__name__, {})) + self.report_failures(failures) if __name__ == '__main__': test = TestFFT()