diff --git a/.gitignore b/.gitignore
index 5963120213e4a657c544fc04be7ba6873d934109..87ff9711bc5e685329e0c348da2cd2e9b9cf8f08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ hysop/f2hysop.pyf
 build/
 debug/
 release/
+__pycache__
 
 .#*
 .DS_Store
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 36578fae99a9faee0d3f279c029a4b33375b4923..66556df9d851d38dfb53906aef6a7c6df00d0a02 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -8,13 +8,13 @@ stages:
   - test
 
 env:bionic:
-  image: keckj/hysop:ubuntu_bionic_cuda
+  image: keckj/hysop:ubuntu_bionic
   stage: env
   script:
       - "sh ci/scripts/version.sh"
 
 config:bionic:
-  image: keckj/hysop:ubuntu_bionic_cuda
+  image: keckj/hysop:ubuntu_bionic
   stage: configure
   script: 
       - "sh ci/scripts/config.sh $CI_PROJECT_DIR/build/gcc-7   $CI_PROJECT_DIR/install/gcc-7   gcc-7     g++-7       gfortran-7"
@@ -26,7 +26,7 @@ config:bionic:
         - $CI_PROJECT_DIR/build
 
 build:bionic:
-  image: keckj/hysop:ubuntu_bionic_cuda
+  image: keckj/hysop:ubuntu_bionic
   stage: build
   script: 
       - "sh ci/scripts/build.sh $CI_PROJECT_DIR/build/gcc-7   gcc-7     g++-7       gfortran-7"
@@ -38,7 +38,7 @@ build:bionic:
         - $CI_PROJECT_DIR/build
 
 install:bionic:
-  image: keckj/hysop:ubuntu_bionic_cuda
+  image: keckj/hysop:ubuntu_bionic
   stage: install
   script: 
       - "sh ci/scripts/install.sh $CI_PROJECT_DIR/build/gcc-7 $CI_PROJECT_DIR/install/gcc-7"
@@ -49,7 +49,7 @@ install:bionic:
         - $CI_PROJECT_DIR/install
 
 test:bionic:
-  image: keckj/hysop:ubuntu_bionic_cuda
+  image: keckj/hysop:ubuntu_bionic
   stage: test
   script:
     - "sh ci/scripts/test.sh $CI_PROJECT_DIR/install/gcc-7 $CI_PROJECT_DIR/hysop $CI_PROJECT_DIR/cache"
@@ -58,5 +58,5 @@ test:bionic:
   cache:
     paths:
       - $CI_PROJECT_DIR/cache
-    key: "test_cache"
+    key: "test_cache_0000"
 
diff --git a/ci/docker_images/ubuntu/bionic/Dockerfile b/ci/docker_images/ubuntu/bionic/Dockerfile
index 1657b8e9a35a705530c4f475301884abdbf060e5..dcd4f1cd110ce34a79048d31e69c72465265eacb 100644
--- a/ci/docker_images/ubuntu/bionic/Dockerfile
+++ b/ci/docker_images/ubuntu/bionic/Dockerfile
@@ -48,6 +48,7 @@ RUN apt-get install -y libcairomm-1.0-dev
 RUN apt-get install -y python
 RUN apt-get install -y python-dev
 RUN apt-get install -y python-pip
+RUN apt-get install -y python-tk
 RUN apt-get install -y opencl-headers
 RUN apt-get install -y ocl-icd-libopencl1
 RUN apt-get install -y clinfo
@@ -79,6 +80,8 @@ RUN pip install --upgrade primefac
 RUN pip install --upgrade pycairo
 RUN pip install --upgrade weave
 RUN pip install --upgrade argparse_color_formatter
+RUN pip install --upgrade numba
+
 
 # scitools (python-scitools does not exist on ubuntu:bionic)
 RUN cd /tmp                                      \
@@ -124,6 +127,7 @@ RUN ldconfig
 
 # pyopencl
 RUN cd /tmp                                       \
+&& pip install pybind11                           \
 && git clone https://github.com/inducer/pyopencl  \
 && cd pyopencl                                    \
 && git submodule update --init                    \
@@ -202,6 +206,7 @@ RUN cd /tmp                                      \
  && git clone https://github.com/drwells/pyFFTW  \
  && cd pyFFTW                                    \
  && git checkout r2r-try-two                     \
+ && sed -i 's/\(fftw3[fl]\?_\)threads/\1omp/g' setup.py \
  && pip install .                                \
  && cd -                                         \
  && rm -Rf /tmp/pyFFTW
@@ -209,8 +214,6 @@ RUN cd /tmp                                      \
 # ensure all libraries are known by the runtime linker
 RUN ldconfig
 
-RUN apt-get update && apt-get install -y python-tk
-
 # clean cached packages
 RUN rm -rf /var/lib/apt/lists/*
 RUN rm -rf $HOME/.cache/pip/*
diff --git a/ci/docker_images/ubuntu/bionic_cuda/Dockerfile b/ci/docker_images/ubuntu/bionic_cuda/Dockerfile
index 8395630a8df5b446c5f0ad65cb5f41a35dfbc40d..ed8e82c8dcdefa3d1b71685b14ab9b09251fb9ac 100644
--- a/ci/docker_images/ubuntu/bionic_cuda/Dockerfile
+++ b/ci/docker_images/ubuntu/bionic_cuda/Dockerfile
@@ -80,6 +80,8 @@ RUN pip install --upgrade primefac
 RUN pip install --upgrade pycairo
 RUN pip install --upgrade weave
 RUN pip install --upgrade argparse_color_formatter
+RUN pip install --upgrade numba
+
 
 # scitools (python-scitools does not exist on ubuntu:bionic)
 RUN cd /tmp                                      \
@@ -125,6 +127,7 @@ RUN ldconfig
 
 # pyopencl
 RUN cd /tmp                                       \
+&& pip install pybind11                           \
 && git clone https://github.com/inducer/pyopencl  \
 && cd pyopencl                                    \
 && git submodule update --init                    \
@@ -203,6 +206,7 @@ RUN cd /tmp                                      \
  && git clone https://github.com/drwells/pyFFTW  \
  && cd pyFFTW                                    \
  && git checkout r2r-try-two                     \
+ && sed -i 's/\(fftw3[fl]\?_\)threads/\1omp/g' setup.py \
  && pip install .                                \
  && cd -                                         \
  && rm -Rf /tmp/pyFFTW
diff --git a/ci/scripts/test.sh b/ci/scripts/test.sh
index 6a2bb7554c248f483af86ac02ae752eb9213d4ca..51b8b970edfa87e2a54dd06852ff62fe867a1cca 100755
--- a/ci/scripts/test.sh
+++ b/ci/scripts/test.sh
@@ -41,51 +41,57 @@ if [ "$HAS_CACHE_DIR" = true ]; then
     fi
 fi
 
-export PYTHONPATH="$INSTALL_DIR/lib/python2.7/site-packages:$PYTHONPATH"
+export PYTHONPATH="$INSTALL_DIR/lib/python2.7/site-packages:$INSTALL_DIR:$PYTHONPATH"
 export HYSOP_VERBOSE=0
 export HYSOP_DEBUG=0
 export HYSOP_PROFILE=0
 export HYSOP_KERNEL_DEBUG=0
 python -c 'import hysop; print hysop'
 
-# long backend dependent tests
+# long backend dependent tests are disabled for ci
 #python "$HYSOP_DIR/backend/device/codegen/kernels/tests/test_directional_advection.py"
 #python "$HYSOP_DIR/backend/device/codegen/kernels/tests/test_directional_remesh.py"
 
 python "$HYSOP_DIR/core/arrays/tests/test_array.py"
+python "$HYSOP_DIR/core/graph/tests/test_graph.py"
 python "$HYSOP_DIR/fields/tests/test_fields.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"
-python "$HYSOP_DIR/operator/tests/test_poisson.py"
-python "$HYSOP_DIR/operator/tests/test_poisson_rotational.py"
-python "$HYSOP_DIR/operator/tests/test_solenoidal_projection.py"
-python "$HYSOP_DIR/operator/tests/test_custom_symbolic.py"
+python "$HYSOP_DIR/operator/tests/test_fd_derivative.py"
+python "$HYSOP_DIR/operator/tests/test_absorption.py"
 python "$HYSOP_DIR/operator/tests/test_directional_advection.py"
 python "$HYSOP_DIR/operator/tests/test_directional_diffusion.py"
-python "$HYSOP_DIR/operator/tests/test_diffusion.py"
 python "$HYSOP_DIR/operator/tests/test_directional_stretching.py"
+python "$HYSOP_DIR/operator/tests/test_custom_symbolic.py"
+python "$HYSOP_DIR/operator/tests/test_spectral_derivative.py"
+python "$HYSOP_DIR/operator/tests/test_spectral_curl.py"
+python "$HYSOP_DIR/operator/tests/test_diffusion.py"
+python "$HYSOP_DIR/operator/tests/test_poisson.py"
+python "$HYSOP_DIR/operator/tests/test_solenoidal_projection.py"
+python "$HYSOP_DIR/operator/tests/test_poisson_curl.py"
 
 # If scales (fortran advection library) is installed
 python -c "from hysop.f2hysop import scales2py as scales" && python "$HYSOP_DIR/operator/tests/test_scales_advection.py"
 python -c "from hysop.f2hysop import scales2py as scales" && python "$HYSOP_DIR/operator/tests/test_bilevel_advection.py"
 
-# export HYSOP_VERBOSE=1
-# EXAMPLE_DIR="$HYSOP_DIR/../examples"
-# EXAMPLE_OPTIONS='-cp default -maxit 1'
-# python "$EXAMPLE_DIR/analytic/analytic.py" $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/scalar_diffusion/scalar_diffusion.py" $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/scalar_advection/scalar_advection.py" $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/shear_layer/shear_layer.py" $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/taylor_green/taylor_green.py" -impl python $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/taylor_green/taylor_green.py" -impl opencl $EXAMPLE_OPTIONS
-# python -c "from hysop.f2hysop import scales2py as scales" && python "$EXAMPLE_DIR/taylor_green/taylor_green.py" -impl fortran $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/bubble/periodic_bubble.py" $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/bubble/periodic_bubble_levelset.py" $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/bubble/periodic_bubble_levelset_penalization.py" $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/particles_above_salt/particles_above_salt_periodic.py" $EXAMPLE_OPTIONS
-# python "$EXAMPLE_DIR/particles_above_salt/particles_above_salt_symmetrized.py" $EXAMPLE_OPTIONS
+#export HYSOP_VERBOSE=1
+#EXAMPLE_DIR="$HYSOP_DIR/../examples"
+#EXAMPLE_OPTIONS='-cp default -maxit 2'
+#python "$EXAMPLE_DIR/analytic/analytic.py" $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/scalar_diffusion/scalar_diffusion.py" $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/scalar_advection/scalar_advection.py" $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/shear_layer/shear_layer.py" $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/taylor_green/taylor_green.py" -impl python $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/taylor_green/taylor_green.py" -impl opencl $EXAMPLE_OPTIONS
+#python -c "from hysop.f2hysop import scales2py as scales" && python "$EXAMPLE_DIR/taylor_green/taylor_green.py" -impl fortran $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/bubble/periodic_bubble.py" $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/bubble/periodic_bubble_levelset.py" $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/bubble/periodic_bubble_levelset_penalization.py" $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/bubble/periodic_jet_levelset.py" $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/particles_above_salt/particles_above_salt_periodic.py" $EXAMPLE_OPTIONS
+#python "$EXAMPLE_DIR/particles_above_salt/particles_above_salt_symmetrized.py" $EXAMPLE_OPTIONS
 
 if [ "$HAS_CACHE_DIR" = true ]; then
     cp -r /root/.cache/* $CACHE_DIR/
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/examples/analytic/analytic.py b/examples/analytic/analytic.py
index f0606961be1e349d31e77d8ed84820479da0d96f..09f01a5cdfcf06c9932af7505a1d80ae1835082c 100755
--- a/examples/analytic/analytic.py
+++ b/examples/analytic/analytic.py
@@ -83,12 +83,12 @@ def compute(args):
                 **op_kwds)
 
     # Write output field at given frequency
-    analytic.dump_outputs(fields=scalar, frequency=args.dump_freq, filename='F')
+    analytic.dump_outputs(fields=scalar, frequency=args.dump_freq, filename='F', **op_kwds)
    
     # Create the problem we want to solve and insert our operator
     problem = Problem()
     problem.insert(analytic)
-    problem.build()
+    problem.build(args)
 
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
diff --git a/examples/bubble/periodic_bubble.py b/examples/bubble/periodic_bubble.py
index 6ae304e04def3f64bb743f9ed532f232f600d580..8029f494f7fde2818c9860565e02ca6970e38285 100644
--- a/examples/bubble/periodic_bubble.py
+++ b/examples/bubble/periodic_bubble.py
@@ -66,7 +66,7 @@ def compute(args):
 
     from hysop.operators import DirectionalAdvection, DirectionalDiffusion,        \
                                 DirectionalStretchingDiffusion,                    \
-                                PoissonRotational, AdaptiveTimeStep,               \
+                                PoissonCurl, AdaptiveTimeStep,               \
                                 Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
                                 ParameterPlotter, Integrate, HDF_Writer,           \
                                 DirectionalSymbolic
@@ -111,7 +111,7 @@ def compute(args):
     # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
     t, dt = TimeParameters(dtype=args.dtype)
     velo  = VelocityField(domain=box, dtype=args.dtype)
-    vorti = VorticityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
     rho   = DensityField(domain=box, dtype=args.dtype) 
     mu    = ViscosityField(domain=box, dtype=args.dtype, mu=True)
 
@@ -191,7 +191,7 @@ def compute(args):
     
     ### Build standard operators
     #> Poisson operator to recover the velocity from the vorticity
-    poisson = PoissonRotational(name='poisson', velocity=velo, vorticity=vorti, 
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
                             variables={velo:npts, vorti: npts}, 
                             projection=args.reprojection_frequency,
                             implementation=impl, **extra_op_kwds)
@@ -262,7 +262,7 @@ def compute(args):
                    # integrate_enstrophy, integrate_rho, integrate_mu,
                    min_max_rho, min_max_mu, min_max_U, min_max_W, 
                    adapt_dt)
-    problem.build()
+    problem.build(args)
 
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
@@ -394,7 +394,7 @@ if __name__=='__main__':
 
     parser = PeriodicBubbleArgParser()
 
-    parser.set_defaults(impl='cl', ndim=2, npts=(257,),
+    parser.set_defaults(impl='cl', ndim=2, npts=(256,),
                         box_origin=(0.0,), box_length=(1.0,), 
                         tstart=0.0, tend=0.51, 
                         dt=1e-5, cfl=0.5, lcfl=0.125,
diff --git a/examples/bubble/periodic_bubble_levelset.py b/examples/bubble/periodic_bubble_levelset.py
index 6ddcae08c93686bf78acaaa8f7da3345758dd9d2..6fcaf328e800ef37e734ecca042fe803b1e18e45 100644
--- a/examples/bubble/periodic_bubble_levelset.py
+++ b/examples/bubble/periodic_bubble_levelset.py
@@ -47,7 +47,7 @@ def compute(args):
 
     from hysop.operators import DirectionalAdvection, DirectionalDiffusion,        \
                                 DirectionalStretchingDiffusion,                    \
-                                PoissonRotational, AdaptiveTimeStep,               \
+                                PoissonCurl, AdaptiveTimeStep,               \
                                 Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
                                 ParameterPlotter, Integrate, HDF_Writer,           \
                                 DirectionalSymbolic
@@ -99,7 +99,7 @@ def compute(args):
     # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
     t, dt = TimeParameters(dtype=args.dtype)
     velo  = VelocityField(domain=box, dtype=args.dtype)
-    vorti = VorticityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
     phi   = LevelSetField(domain=box, dtype=args.dtype)
     rho   = DensityField(domain=box, dtype=args.dtype) 
     mu    = ViscosityField(domain=box, dtype=args.dtype, mu=True)
@@ -200,7 +200,7 @@ def compute(args):
     
     ### Build standard operators
     #> Poisson operator to recover the velocity from the vorticity
-    poisson = PoissonRotational(name='poisson', velocity=velo, vorticity=vorti, 
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
                             variables={velo:npts, vorti: npts}, 
                             projection=args.reprojection_frequency,
                             implementation=impl, **extra_op_kwds)
@@ -265,7 +265,7 @@ def compute(args):
                    integrate_enstrophy, integrate_rho, integrate_mu,
                    min_max_U, min_max_W, 
                    adapt_dt)
-    problem.build()
+    problem.build(args)
 
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
@@ -394,7 +394,7 @@ if __name__=='__main__':
 
     parser = PeriodicBubbleArgParser()
 
-    parser.set_defaults(impl='cl', ndim=2, npts=(257,),
+    parser.set_defaults(impl='cl', ndim=2, npts=(256,),
                         box_origin=(0.0,), box_length=(1.0,), 
                         tstart=0.0, tend=0.51, 
                         dt=1e-6, cfl=0.5, lcfl=0.125,
diff --git a/examples/bubble/periodic_bubble_levelset_penalization.py b/examples/bubble/periodic_bubble_levelset_penalization.py
index 1c6e8b72d1296781bc332e953e4278d7aae36801..5732752c4647012cd81bd9454c2400e17223aa22 100644
--- a/examples/bubble/periodic_bubble_levelset_penalization.py
+++ b/examples/bubble/periodic_bubble_levelset_penalization.py
@@ -53,7 +53,7 @@ def compute(args):
 
     from hysop.operators import DirectionalAdvection, DirectionalDiffusion,        \
                                 DirectionalStretchingDiffusion,                    \
-                                PoissonRotational, AdaptiveTimeStep,               \
+                                PoissonCurl, AdaptiveTimeStep,               \
                                 Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
                                 ParameterPlotter, Integrate, HDF_Writer,           \
                                 DirectionalSymbolic
@@ -107,7 +107,7 @@ def compute(args):
     # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
     t, dt   = TimeParameters(dtype=args.dtype)
     velo    = VelocityField(domain=box, dtype=args.dtype)
-    vorti   = VorticityField(domain=box, dtype=args.dtype)
+    vorti   = VorticityField(velocity=velo)
     phi     = LevelSetField(domain=box, dtype=args.dtype)
     _lambda = PenalizationField(domain=box, dtype=args.dtype)
     rho     = DensityField(domain=box, dtype=args.dtype) 
@@ -225,7 +225,7 @@ def compute(args):
     
     ### Build standard operators
     #> Poisson operator to recover the velocity from the vorticity
-    poisson = PoissonRotational(name='poisson', velocity=velo, vorticity=vorti, 
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
                             variables={velo:npts, vorti: npts}, 
                             projection=args.reprojection_frequency,
                             implementation=impl, **extra_op_kwds)
@@ -276,7 +276,9 @@ def compute(args):
     max_dt = min(W0_dt, W1_dt)
     adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True, max_dt=max_dt,
                                     name='merge_dt', pretty_name='dt', )
-    dt_cfl   = adapt_dt.push_cfl_criteria(cfl=args.cfl, Finf=min_max_U.Finf,
+    dt_cfl   = adapt_dt.push_cfl_criteria(cfl=args.cfl, 
+                                            Fmin=min_max_U.Fmin,
+                                            Fmax=min_max_U.Fmax,
                                             equivalent_CFL=True, 
                                             name='dt_cfl', pretty_name='CFL')
     dt_advec = adapt_dt.push_advection_criteria(lcfl=args.lcfl, Finf=min_max_W.Finf,
@@ -303,7 +305,7 @@ def compute(args):
                    integrate_enstrophy, integrate_rho, integrate_mu,
                    min_max_U, min_max_W, 
                    adapt_dt)
-    problem.build()
+    problem.build(args)
 
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
@@ -433,7 +435,7 @@ if __name__=='__main__':
 
     parser = PeriodicBubbleArgParser()
 
-    parser.set_defaults(impl='cl', ndim=2, npts=(244,),
+    parser.set_defaults(impl='cl', ndim=2, npts=(256,),
                         box_origin=(0.0,), box_length=(1.0,), 
                         tstart=0.0, tend=1.75, 
                         dt=1e-6, cfl=0.5, lcfl=0.125,
diff --git a/examples/bubble/periodic_jet_levelset.py b/examples/bubble/periodic_jet_levelset.py
index 2d1653b75c33ebf0312af9f527ad98116fde5af4..9f4ba310f2348a1844db8c205dfa883c3dcce464 100644
--- a/examples/bubble/periodic_jet_levelset.py
+++ b/examples/bubble/periodic_jet_levelset.py
@@ -41,7 +41,7 @@ def compute(args):
 
     from hysop.operators import DirectionalAdvection, DirectionalDiffusion,        \
                                 DirectionalStretchingDiffusion,                    \
-                                PoissonRotational, AdaptiveTimeStep,               \
+                                PoissonCurl, AdaptiveTimeStep,               \
                                 Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
                                 ParameterPlotter, Integrate, HDF_Writer,           \
                                 DirectionalSymbolic
@@ -93,7 +93,7 @@ def compute(args):
     # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
     t, dt = TimeParameters(dtype=args.dtype)
     velo  = VelocityField(domain=box, dtype=args.dtype)
-    vorti = VorticityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
     phi   = LevelSetField(domain=box, dtype=args.dtype)
     rho   = DensityField(domain=box, dtype=args.dtype) 
 
@@ -195,7 +195,7 @@ def compute(args):
     
     ### Build standard operators
     #> Poisson operator to recover the velocity from the vorticity
-    poisson = PoissonRotational(name='poisson', velocity=velo, vorticity=vorti, 
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
                             variables={velo:npts, vorti: npts}, 
                             projection=args.reprojection_frequency,
                             implementation=impl, **extra_op_kwds)
@@ -255,7 +255,7 @@ def compute(args):
                    integrate_enstrophy, integrate_rho,
                    min_max_U, min_max_W, 
                    adapt_dt)
-    problem.build()
+    problem.build(args)
 
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
@@ -339,7 +339,7 @@ if __name__=='__main__':
 
     parser = PeriodicJetArgParser()
 
-    parser.set_defaults(impl='cl', ndim=2, npts=(129,),
+    parser.set_defaults(impl='cl', ndim=2, npts=(128,),
                         box_origin=(0.0,), box_length=(1.0,), 
                         tstart=0.0, tend=0.66, 
                         dt=1e-5, cfl=0.5, lcfl=0.125,
diff --git a/examples/cylinder/oscillating_cylinder.py b/examples/cylinder/oscillating_cylinder.py
new file mode 100644
index 0000000000000000000000000000000000000000..e4a34206cf2e6c9f8b295433f1d1b534d499d01e
--- /dev/null
+++ b/examples/cylinder/oscillating_cylinder.py
@@ -0,0 +1,301 @@
+
+## HySoP Example: Oscillating cylinder
+## Quentin Desbonnets
+## PhD: Methode d'homogeneisation pour la vibration de faisceaux de tubes en presence de fluide
+## Example for only one cylinder
+
+import os
+import numpy as np
+
+def init_vorticity(data, coords):
+    for d in data:
+        d[...] = 0.0
+
+def init_velocity(data, coords, **kwds):
+    for d in data:
+        d[...] = 0.0
+
+def init_lambda(data, coords):
+    for d in data:
+        d[...] = 0.0
+
+def compute(args):
+    from hysop import Box, Simulation, Problem, MPIParams, IOParams, vprint, \
+                      ScalarParameter
+    from hysop.defaults import VelocityField, VorticityField, \
+                               DensityField, ViscosityField, \
+                               LevelSetField, PenalizationField, \
+                               EnstrophyParameter, TimeParameters, \
+                               VolumicIntegrationParameter
+    from hysop.constants import Implementation, AdvectionCriteria
+
+    from hysop.operators import DirectionalAdvection, DirectionalDiffusion,        \
+                                DirectionalStretching,                             \
+                                PoissonCurl, AdaptiveTimeStep,                     \
+                                Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
+                                ParameterPlotter, HDF_Writer,                      \
+                                DirectionalSymbolic, AnalyticField
+
+    from hysop.methods import SpaceDiscretization, Remesh, TimeIntegrator, \
+                              ComputeGranularity, Interpolation
+    from hysop.numerics.odesolvers.runge_kutta import Euler, RK2, RK3, RK4
+    
+    from hysop.symbolic import sm, space_symbols, local_indices_symbols
+    from hysop.symbolic.base import SymbolicTensor
+    from hysop.symbolic.field import curl
+    from hysop.symbolic.relational import Assignment, LogicalGT, LogicalLT, LogicalAND
+    from hysop.symbolic.misc import Select
+    from hysop.symbolic.tmp import TmpScalar
+    from hysop.tools.string_utils import framed_str
+    
+    Kc = 5
+    Re = 250
+
+    T = 1.0
+    D = 1.0
+    E = 1.7
+
+    H = 2*E
+    L = 2*H
+    N = 1024
+
+    Vc = Kc*D/T
+    mu = Vc*D/Re
+    mu = ScalarParameter(name='mu', dtype=args.dtype, const=True, initial_value=mu)
+
+    # Define the domain
+    dim  = 2
+    npts = (int(H/E)*N, int(L/E)*N)
+    box  = Box(origin=(-H/2, -L/2), length=(H,L), dim=dim)
+    
+    # Get default MPI Parameters from domain (even for serial jobs)
+    mpi_params = MPIParams(comm=box.task_comm,
+                           task_id=box.current_task())
+    
+    # Setup usual implementation specific variables
+    impl = args.impl
+    extra_op_kwds = {'mpi_params': mpi_params}
+    if (impl is Implementation.PYTHON):
+        method = {}
+    elif (impl is Implementation.OPENCL):
+        # For the OpenCL implementation we need to setup the compute device
+        # and configure how the code is generated and compiled at runtime.
+                
+        # Create an explicit OpenCL context from user parameters
+        from hysop.backend.device.opencl.opencl_tools import get_or_create_opencl_env
+        cl_env = get_or_create_opencl_env(mpi_params=mpi_params, 
+                                          platform_id=args.cl_platform_id, 
+                                          device_id=args.cl_device_id)
+        
+        # Configure OpenCL kernel generation and tuning (already done by HysopArgParser)
+        from hysop.methods import OpenClKernelConfig
+        method = { OpenClKernelConfig: args.opencl_kernel_config }
+        
+        # Setup opencl specific extra operator keyword arguments
+        extra_op_kwds['cl_env'] = cl_env
+    else:
+        msg='Unknown implementation \'{}\'.'.format(impl)
+        raise ValueError(msg)
+    
+    # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
+    t, dt   = TimeParameters(dtype=args.dtype)
+    velo    = VelocityField(domain=box, dtype=args.dtype)
+    vorti   = VorticityField(velocity=velo)
+    _lambda = PenalizationField(domain=box, dtype=args.dtype)
+
+    # Symbolic fields
+    frame = velo.domain.frame
+    Us    = velo.s(*frame.vars)
+    Ws    = vorti.s(*frame.vars)
+    lambdas = _lambda.s(*frame.vars)
+    ts    = t.s
+    dts   = dt.s
+
+    # Cylinder configuration
+    X, Y = space_symbols[:2]
+    Xc = (Kc/(2*np.pi)*D) * sm.cos(2*np.pi*ts/T)
+    Yc = 0.0
+    Uc = np.asarray([Xc.diff(ts), 0.0])
+    Xs = LogicalLT((Xc-X)**2 + (Yc-Y)**2,  D**2/4)
+
+    compute_lambda = 1e8*Xs
+    cylinder = AnalyticField(name='cylinder', 
+                field=_lambda, formula=compute_lambda,
+                variables = {_lambda: npts}, implementation=impl,
+                **extra_op_kwds)
+    
+    ### Build the directional operators
+    #> Directional penalization
+    penalization = +dts*lambdas*(Uc-Us) / (1+lambdas*dts)
+    penalization = penalization.freeze()
+    lhs = Ws
+    rhs = curl(penalization, frame)
+    exprs = Assignment.assign(lhs, rhs)
+    penalization = DirectionalSymbolic(name='penalization', 
+                                    implementation=impl,
+                                    exprs=exprs,
+                                    fixed_residue=Ws,
+                                    variables={vorti: npts, velo: npts, _lambda: npts},
+                                    method={TimeIntegrator: Euler},
+                                    dt=dt, **extra_op_kwds)
+    #> Directional advection 
+    advec = DirectionalAdvection(implementation=impl,
+            name='advection',
+            pretty_name='Adv',
+            velocity = velo,       
+            advected_fields = (vorti,),
+            velocity_cfl = args.cfl,
+            variables = {velo: npts, vorti: npts},
+            dt=dt, **extra_op_kwds)
+    
+    #> Directional stretching + diffusion
+    if (dim==3):
+        stretch = DirectionalStretching(implementation=impl,
+                 name='stretch',
+                 pretty_name='stretch',
+                 formulation = args.stretching_formulation,
+                 velocity  = velo,       
+                 vorticity = vorti,
+                 variables = {velo: npts, vorti: npts},
+                 dt=dt, **extra_op_kwds)
+    else:
+        stretch = None
+    
+
+    #> Directional splitting operator subgraph
+    splitting = StrangSplitting(splitting_dim=dim, order=args.strang_order)
+    splitting.push_operators(penalization, advec, stretch)
+    
+    ### Build standard operators
+    #> Poisson operator to recover the velocity from the vorticity
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
+                            variables={velo:npts, vorti: npts}, 
+                            diffusion=mu, dt=dt,
+                            projection=args.reprojection_frequency,
+                            implementation=impl, **extra_op_kwds)
+
+    #> Operators to dump rho and mu
+    io_params = IOParams(filename='fields', frequency=args.dump_freq)
+    dump_fields = HDF_Writer(name='dump',
+                             io_params=io_params,
+                             variables={velo: npts, 
+                                        vorti: npts,    
+                                        _lambda: npts})
+    
+    #> Operator to compute the infinite norm of the velocity
+    min_max_U = MinMaxFieldStatistics(field=velo,
+            Finf=True, implementation=impl, variables={velo:npts},
+            **extra_op_kwds)
+    #> Operator to compute the infinite norm of the vorticity
+    min_max_W = MinMaxFieldStatistics(field=vorti,
+            Finf=True, implementation=impl, variables={vorti:npts},
+            **extra_op_kwds)
+
+
+    ### Adaptive timestep operator
+    dx = np.min(np.divide(box.length, np.asarray(npts)-1))
+    CFL_dt = (args.cfl*dx)/Kc
+    msg = 'CFL_dt={}'
+    msg = msg.format(CFL_dt)
+    msg = '\n'+framed_str(' CYLINDER EVOLUTION STABILITY CRITERIA ', msg)
+    vprint(msg)
+    max_dt = CFL_dt
+    adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True, max_dt=max_dt,
+                                    name='merge_dt', pretty_name='dt', )
+    dt_cfl   = adapt_dt.push_cfl_criteria(cfl=args.cfl, 
+                                          Fmin=min_max_U.Fmin,
+                                          Fmax=min_max_U.Fmax,
+                                          equivalent_CFL=True, 
+                                          name='dt_cfl', pretty_name='CFL')
+    dt_advec = adapt_dt.push_advection_criteria(lcfl=args.lcfl, Finf=min_max_W.Finf,
+                                                    criteria=AdvectionCriteria.W_INF,
+                                                 name='dt_lcfl', pretty_name='LCFL')
+    
+    ## Create the problem we want to solve and insert our 
+    # directional splitting subgraph and the standard operators.
+    # The method dictionnary passed to this graph will be dispatched
+    # accross all operators contained in the graph.
+    method.update(
+            { 
+               ComputeGranularity:    args.compute_granularity,
+               SpaceDiscretization:   args.fd_order,
+               TimeIntegrator:        args.time_integrator,
+               Remesh:                args.remesh_kernel,
+               Interpolation:         args.interpolation
+            }
+    )
+    problem = Problem(method=method)
+    problem.insert(cylinder,
+                   poisson, 
+                   splitting, 
+                   dump_fields,
+                   min_max_U, min_max_W, 
+                   adapt_dt)
+    problem.build(args)
+
+    # If a visu_rank was provided, and show_graph was set,
+    # display the graph on the given process rank.
+    if args.display_graph:
+        problem.display(args.visu_rank)
+    
+    # Create a simulation
+    # (do not forget to specify the t and dt parameters here)
+    simu = Simulation(start=args.tstart, end=args.tend, 
+                      nb_iter=args.nb_iter,
+                      max_iter=args.max_iter,
+                      dt0=args.dt, times_of_interest=args.dump_times,
+                      t=t, dt=dt)
+    simu.write_parameters(t, dt_cfl, dt_advec, dt, 
+            min_max_U.Finf, min_max_W.Finf, 
+            adapt_dt.equivalent_CFL,
+            filename='parameters.txt', precision=8)
+    
+    # Initialize vorticity, velocity, viscosity and density on all topologies
+    problem.initialize_field(field=velo,    formula=init_velocity)
+    problem.initialize_field(field=vorti,   formula=init_vorticity)
+    problem.initialize_field(field=_lambda, formula=init_lambda)
+
+    # Finally solve the problem 
+    problem.solve(simu, dry_run=args.dry_run)
+    
+    # Finalize
+    problem.finalize()
+
+
+if __name__=='__main__':
+    from examples.example_utils import HysopArgParser, colors
+
+    class OscillatingCylinderArgParser(HysopArgParser):
+        def __init__(self):
+            prog_name = 'oscillating_cylinder'
+            default_dump_dir = '{}/hysop_examples/{}'.format(HysopArgParser.tmp_dir(), 
+                    prog_name)
+
+            description=colors.color('HySoP Oscillating Cylinder Example: ', fg='blue', style='bold')
+            description+='\n'
+            description+='\nThis example focuses on a validation study for the '
+            description+='penalization with a immersed moving cylinder boundary.'
+    
+            super(OscillatingCylinderArgParser, self).__init__(
+                 prog_name=prog_name,
+                 description=description,
+                 default_dump_dir=default_dump_dir)
+        
+        def _setup_parameters(self, args):
+            dim = args.ndim
+            if (dim not in (2,3)):
+                msg='Domain should be 2D or 3D.'
+                self.error(msg)
+
+
+    parser = OscillatingCylinderArgParser()
+
+    toi = tuple(np.linspace(0.0, 20.0, 20*24).tolist())
+
+    parser.set_defaults(impl='cl', ndim=2,
+                        tstart=0.0, tend=20.1, 
+                        dt=1e-6, cfl=0.5, lcfl=0.95,
+                        dump_freq=0, 
+                        dump_times=toi)
+
+    parser.run(compute)
diff --git a/examples/example_utils.py b/examples/example_utils.py
index 30ee3c338814d01bde2683f632be4d2f68018120..909ebdbcb657c6cc5f72c6a258a79c583647572a 100644
--- a/examples/example_utils.py
+++ b/examples/example_utils.py
@@ -1,4 +1,4 @@
-import os, argparse, tempfile, colors, textwrap, warnings, contextlib, tee, re, errno, shutil
+import os, argparse, tempfile, colors, textwrap, warnings, contextlib, tee, re, errno, shutil, psutil
 from argparse_color_formatter import ColorHelpFormatter
 
 # Fix a bug in the tee module #########
@@ -83,8 +83,9 @@ class HysopArgParser(argparse.ArgumentParser):
         return (cls.get_fs_type(path) in ('nfs',))
 
     @classmethod
-    def set_env(cls, target, value):
-        target = 'HYSOP_{}'.format(target)
+    def set_env(cls, target, value, hysop=True):
+        if hysop:
+            target = 'HYSOP_{}'.format(target)
         if (value is None):
             pass
         elif (value is True):
@@ -93,6 +94,8 @@ class HysopArgParser(argparse.ArgumentParser):
             os.environ[target] = '0'
         elif isinstance(value, str):
             os.environ[target] = value
+        elif isinstance(value, int):
+            os.environ[target] = str(value)
         else:
             msg='Invalid value of type {}.'.format(type(value))
             raise TypeError(msg)
@@ -134,7 +137,9 @@ class HysopArgParser(argparse.ArgumentParser):
         self._add_main_args()
         self._add_domain_args()
         self._add_simu_args()
+        self._add_problem_args()
         self._add_method_args()
+        self._add_threading_args()
         self._add_opencl_args()
         self._add_autotuner_args()
         self._add_graphical_io_args()
@@ -145,6 +150,8 @@ class HysopArgParser(argparse.ArgumentParser):
     def parse(self): 
         args = self.parse_args()
         args.__class__ = HysopNamespace
+        
+        self._check_threading_args(args)
 
         self._setup_hysop_env(args)
         
@@ -155,6 +162,7 @@ class HysopArgParser(argparse.ArgumentParser):
         self._check_positional_args(args)
         self._check_main_args(args)
         self._check_domain_args(args)
+        self._check_problem_args(args)
         self._check_simu_args(args)
         self._check_method_args(args)
         self._check_opencl_args(args)
@@ -289,10 +297,13 @@ class HysopArgParser(argparse.ArgumentParser):
         args = self.add_argument_group('Main parameters')
         args.add_argument('-impl', '--implementation', type=str, default='python',
                 dest='impl',
-                help='Backend implementation (either python or opencl).')
+                help='Backend implementation (either python, fortran or opencl).')
         args.add_argument('-cp', '--compute-precision', type=str, default='fp32',
                 dest='compute_precision', 
                 help='Floating-point precision used to discretize the parameters and fields.')
+        args.add_argument('-ei', '--enforce-implementation', type=str, default='true',
+                dest='enforce_implementation',
+                help='If set to false, the library may use another implementation than user specified one for some operators.')
         return args
 
     def _check_main_args(self, args):
@@ -301,6 +312,7 @@ class HysopArgParser(argparse.ArgumentParser):
         args.compute_precision = self._convert_precision('compute_precision', 
                                                             args.compute_precision)
         args.dtype = self._precision_to_dtype('compute_precision', args.compute_precision)
+        args.enforce_implementation = self._convert_bool('enforce_implementation', args.enforce_implementation)
 
     def _add_domain_args(self):
         discretization = self.add_argument_group('Discretization parameters')
@@ -398,7 +410,32 @@ class HysopArgParser(argparse.ArgumentParser):
         simu.add_argument('-dr', '--dry-run', default=False, action='store_true',
                 dest='dry_run',
                 help='Stop execution before the first simulation iteration.')
-        return simu
+        
+    def _add_problem_args(self):
+        problem = self.add_argument_group('Problem parameters')
+        problem.add_argument('-stopi', '--stop-at-initialization', default=False, action='store_true',
+                dest='stop_at_initialization',
+                help='Stop execution before problem initialization.')
+        problem.add_argument('-stopd', '--stop-at-discretization', default=False, action='store_true',
+                dest='stop_at_discretization',
+                help='Stop execution before problem discretization.')
+        problem.add_argument('-stopwp', '--stop-at-work-properties', default=False, action='store_true',
+                dest='stop_at_work_properties',
+                help='Stop execution before problem work properties retrieval.')
+        problem.add_argument('-stopwa', '--stop-at-work-allocation', default=False, action='store_true',
+                dest='stop_at_work_allocation',
+                help='Stop execution before problem work properties allocation.')
+        problem.add_argument('-stops', '--stop-at-setup', default=False, action='store_true',
+                dest='stop_at_setup',
+                help='Stop execution before problem setup.')
+        problem.add_argument('-stopb', '--stop-at-build', default=False, action='store_true',
+                dest='stop_at_build',
+                help='Stop execution once the problem has been built.')
+        return problem
+
+    def _check_problem_args(self, args):
+        self._check_default(args, ('stop_at_initialization', 'stop_at_discretization', 'stop_at_setup', 
+                                    'stop_at_work_properties', 'stop_at_work_allocation', 'stop_at_build'), bool, allow_none=False)
 
     def _check_simu_args(self, args):
         self._check_default(args, ('tstart', 'tend'), float)
@@ -472,6 +509,44 @@ class HysopArgParser(argparse.ArgumentParser):
         args.interpolation = self._convert_interpolation('interpolation', 
                 args.interpolation)
         
+    def _add_threading_args(self):
+        threading = self.add_argument_group('threading parameters')
+        msg =  "Enable threads for backends that supports it (Numba and FFTW) by setting HYSOP_ENABLE_THREADS. "
+        msg += "Disabling threading will limit all threading backends to one thread and set numba default backend to 'cpu' instead of 'parallel'."
+        threading.add_argument('--enable-threading', type=str, default='1', 
+                dest='enable_threading',
+                help=msg)
+        msg='Set the default maximum usable threads for threading backends (OpenMP, MKL) and operator backends using threads (Numba, FFTW). '
+        msg+='This parameter will set HYSOP_MAX_THREADS and does not affect the OpenCL backend.'
+        msg+="If this parameter is set to 'physical', the maximum number of threads will be set to the number of physical cores available to the process (taking into account the cpuset). "
+        msg+="If set to 'logical', logical cores will be chosen instead. Else this parameter expects a positive integer."
+        msg+='If --enable-threads is set to False, this parameter is ignored and HYSOP_MAX_THREADS will be set to 1.'
+        threading.add_argument('--max-threads', type=str, default='physical', 
+                dest='max_threads',
+                help=msg)
+        threading.add_argument('--openmp-threads', type=str, default=None, 
+                dest='openmp_threads',
+                help='This parameter will set OMP_NUM_THREADS to a custom value (overrides --max-threads).')
+        threading.add_argument('--mkl-threads', type=str, default=None, 
+                dest='mkl_threads',
+                help='This parameter will set MKL_NUM_THREADS to a custom value (overrides --max-threads).')
+        threading.add_argument('--numba-threads', type=str, default=None, 
+                dest='numba_threads',
+                help='This parameter will set NUMBA_NUM_THREADS to a custom value (overrides --max-threads).')
+        threading.add_argument('--numba-threading-layer', type=str, default='workqueue', 
+                dest='numba_threading_layer',
+                help="This parameter will set NUMBA_THREADING_LAYER to a custom value ('workqueue' is available on all platforms, but not 'omp' and 'tbb'). Use 'numba -s' to list available numba threading layers.")
+        threading.add_argument('--fftw-threads', type=str, default=None, 
+              dest='fftw_threads',
+              help='This parameter will set HYSOP_FFTW_NUM_THREADS to a custom value (overrides --max-threads).')
+        threading.add_argument('--fftw-planner-effort', type=str, default='estimate', 
+              dest='fftw_planner_effort',
+              help='Set default planning effort for FFTW plans. The actual number of threads used by FFTW may depend on the planning step. This parameter will set HYSOP_FFTW_PLANNER_EFFORT.')
+        threading.add_argument('--fftw-planner-timelimit', type=str, default='-1', 
+              dest='fftw_planner_timelimit',
+              help='Set an approximate upper bound in seconds for FFTW planning. This parameter will set HYSOP_FFTW_PLANNER_TIMELIMIT.')
+        return threading
+
 
     def _add_opencl_args(self):
         opencl = self.add_argument_group('OpenCL parameters')
@@ -509,6 +584,25 @@ class HysopArgParser(argparse.ArgumentParser):
                 dest='cl_enable_loop_unrolling',
                 help='Enable loop unrolling for code-generated OpenCL kernels.')
         return opencl
+    
+    def _check_threading_args(self, args):
+        self._check_default(args, ('enable_threading', 'max_threads', 'numba_threading_layer',
+                                   'fftw_planner_effort', 'fftw_planner_timelimit'), str, allow_none=False)
+        self._check_default(args, ('openmp_threads', 'mkl_threads', 'numba_threads', 'fftw_threads'), 
+                            str, allow_none=True)
+
+        args.enable_threading = self._convert_bool('enable_threading', args.enable_threading)
+        if args.enable_threading:
+            args.max_threads = self._convert_threads('max_threads', args.max_threads, default=None)
+        else:
+            args.max_threads = 1
+        for argname in ('openmp_threads', 'mkl_threads', 'numba_threads', 'fftw_threads'):
+            setattr(args, argname, self._convert_threads(argname, getattr(args, argname), 
+                default=args.max_threads))
+        args.numba_threading_layer = self._convert_numba_threading_layer('numba_threading_layer', 
+                args.numba_threading_layer)
+        args.fftw_planner_effort = self._convert_fftw_planner_effort('fftw_planner_effort',
+                args.fftw_planner_effort)
 
     def _check_opencl_args(self, args):
         self._check_default(args, ('cl_platform_id', 'cl_device_id'), int, allow_none=True)
@@ -923,6 +1017,18 @@ class HysopArgParser(argparse.ArgumentParser):
                     msg='Uknown tracing module \'{}\'.'.format(module)
                     self.error(msg)
 
+        self.set_env('ENABLE_THREADING',       args.enable_threading,          True)
+        self.set_env('MAX_THREADS',            args.max_threads,               True)
+        self.set_env('FFTW_NUM_THREADS',       args.fftw_threads,              True)
+        self.set_env('FFTW_PLANNER_EFFORT',    args.fftw_planner_effort,       True)
+        self.set_env('FFTW_PLANNER_TIMELIMIT', args.fftw_planner_timelimit,    True)
+    
+        # those environment variables are not part of HySoP
+        self.set_env('OMP_NUM_THREADS',        args.openmp_threads,            False)
+        self.set_env('MKL_NUM_THREADS',        args.mkl_threads,               False)
+        self.set_env('NUMBA_NUM_THREADS',      args.numba_threads,             False)
+        self.set_env('NUMBA_THREADING_LAYER',  args.numba_threading_layer,     False)
+
     def _setup_parameters(self, args):
         pass
 
@@ -972,6 +1078,42 @@ class HysopArgParser(argparse.ArgumentParser):
             '1':     True,
         }
         return self._check_convert(argname, val, values)
+    
+    def _convert_threads(self, argname, val, default):
+        if (val == 'physical'):
+            val = psutil.cpu_count(logical=False)
+        elif (val == 'logical'):
+            val = psutil.cpu_count(logical=True)
+        elif (val is None):
+            if (default is None):
+                msg = "'Parameter '{}' has been set to None and no default value has been set."
+                msg = msg.format(argname)
+                self.error(msg)
+            else:
+                val = default
+        val = int(val)
+        if not (val > 0): 
+           msg = "'Parameter '{}' has been set to an invalid number of threads {}."
+           msg = msg.format(argname, val)
+           self.error(msg) 
+        return val
+
+    def _convert_fftw_planner_effort(self, argname, val):
+        values = {
+            'estimate':   'FFTW_ESTIMATE',
+            'measure':    'FFTW_MEASURE',
+            'patient':    'FFTW_PATIENT',
+            'exhaustive': 'FFTW_EXHAUSTIVE',
+        }
+        return self._check_convert(argname, val, values)
+    
+    def _convert_numba_threading_layer(self, argname, val):
+        values = {
+            'workqueue': 'workqueue',
+            'omp':       'omp',
+            'tbb':       'tbb'
+        }
+        return self._check_convert(argname, val, values)
 
     def _convert_implementation(self, argname, impl):
         from hysop.constants import Implementation
@@ -1126,6 +1268,9 @@ class HysopHelpFormatter(ColorHelpFormatter):
                     '--version', '--hardware-info', '--hardware-statistics')
             p  = not action.option_strings[0].startswith('--opencl')
             p &= not action.option_strings[0].startswith('--autotuner')
+            p &= not action.option_strings[0].startswith('--fftw')
+            p &= 'thread' not in action.option_strings[0]
+            p &= 'stop' not in action.option_strings[0]
             p &= (action.option_strings[0] not in blacklist)
             p &= (len(action.option_strings)<2) or (action.option_strings[1] not in blacklist)
             return p
diff --git a/examples/flow_around_sphere/flow_around_sphere.py b/examples/flow_around_sphere/flow_around_sphere.py
index 21972284732374a67d764597d8af5998e3746748..5c47ad9d73d103d01a7ef96c2e9d01613272aaaa 100644
--- a/examples/flow_around_sphere/flow_around_sphere.py
+++ b/examples/flow_around_sphere/flow_around_sphere.py
@@ -8,7 +8,7 @@ from hysop.parameters.tensor_parameter import TensorParameter
 from hysop.constants import Implementation, AdvectionCriteria, HYSOP_REAL, \
     StretchingFormulation, StretchingCriteria
 from hysop.operators import Advection, StaticDirectionalStretching, Diffusion, \
-                            PoissonRotational, AdaptiveTimeStep,                  \
+                            PoissonCurl, AdaptiveTimeStep,                  \
                             Enstrophy, MinMaxFieldStatistics, StrangSplitting,    \
                             ParameterPlotter, PenalizeVorticity, FlowRateCorrection, \
                             VorticityAbsorption, CustomOperator
@@ -16,7 +16,7 @@ from hysop.numerics.odesolvers.runge_kutta import RK2
 from hysop.methods import SpaceDiscretization, Remesh, TimeIntegrator, \
                           ComputeGranularity, Interpolation, StrangOrder
 from hysop.topology.cartesian_topology import CartesianTopology
-from hysop.tools.parameters import Discretization
+from hysop.tools.parameters import CartesianDiscretization
 
 
 pi  = np.pi
@@ -26,7 +26,7 @@ sin = np.sin
 
 # Define the domain
 dim = 3
-npts = (33,33,65)
+npts = (32,32,64)
 box = Box(dim=dim,
           origin=[-2.56, -2.56, -2.56],
           length=[5.12, 5.12, 10.24])
@@ -110,7 +110,7 @@ method = {}
 # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
 t, dt = TimeParameters(dtype=HYSOP_REAL)
 velo = VelocityField(domain=box, dtype=HYSOP_REAL)
-vorti = VorticityField(domain=box, dtype=HYSOP_REAL)
+vorti = VorticityField(velocity=velo, dtype=HYSOP_REAL)
 sphere = Field(domain=box, name="Sphere", is_vector=False, dtype=HYSOP_REAL)
 wdotw = Field(domain=box, dtype=HYSOP_REAL, is_vector=False, name="WdotW")
 enstrophy = EnstrophyParameter(dtype=HYSOP_REAL)
@@ -120,11 +120,13 @@ flowrate = TensorParameter(name="flowrate", dtype=HYSOP_REAL, shape=(3, ),
 
 # Topologies
 topo_nogh = CartesianTopology(domain=box,
-                              discretization=Discretization(npts),
+                              discretization=CartesianDiscretization(npts, 
+                                  default_boundaries=True),
                               mpi_params=mpi_params,
                               cutdirs=[False, False, True])
 topo_gh = CartesianTopology(domain=box,
-                            discretization=Discretization(npts, ghosts=(4, 4, 4)),
+                            discretization=CartesianDiscretization(npts, 
+                                ghosts=(4, 4, 4), default_boundaries=True),
                             mpi_params=mpi_params,
                             cutdirs=[False, False, True])
 
@@ -163,7 +165,7 @@ penal = PenalizeVorticity(
 diffuse = Diffusion(
     implementation=Implementation.FORTRAN,
     name='diffuse',
-    viscosity=viscosity,
+    nu=viscosity,
     Fin=vorti,
     variables={vorti: topo_nogh},
     dt=dt, **extra_op_kwds)
@@ -177,7 +179,7 @@ absorption = VorticityAbsorption(
     variables={velo: topo_nogh, vorti: topo_nogh},
     dt=dt, **extra_op_kwds)
 #> Poisson operator to recover the velocity from the vorticity
-poisson = PoissonRotational(
+poisson = PoissonCurl(
     implementation=Implementation.FORTRAN,
     name='poisson',
     velocity=velo,
diff --git a/examples/particles_above_salt/particles_above_salt_bc.py b/examples/particles_above_salt/particles_above_salt_bc.py
new file mode 100644
index 0000000000000000000000000000000000000000..73a0491c389c9a52da0c080acb1c41830a486caa
--- /dev/null
+++ b/examples/particles_above_salt/particles_above_salt_bc.py
@@ -0,0 +1,370 @@
+## See Meiburg 2012 & 2014
+## Sediment-laden fresh water above salt water.
+    
+import numpy as np
+import scipy as sp
+import sympy as sm
+
+# initialize vorticity
+def init_vorticity(data, coords, component=None):
+    # the flow is initially quiescent
+    for d in data:
+        d[...] = 0.0
+
+# initialize velocity
+def init_velocity(data, coords, component=None):
+    # the flow is initially quiescent
+    for d in data:
+        d[...] = 0.0
+
+# initialize sediment concentration and salinity
+def delta(*coords):
+    d = np.prod(*coords)
+    return np.zeros_like(d)
+
+def delta(Ys, l0):
+    Y0 = 1
+    for Yi in Ys:
+        Y0 = Y0*Yi
+    return 0.1*l0*(np.random.rand(*Y0.shape)-0.5)
+    
+def init_concentration(data, coords, l0):
+    coords, = coords
+    X = coords[0]
+    Ys = coords[0:]
+    data[0][...] = 0.5*(1.0 +
+            sp.special.erf((X-delta(Ys,l0))/l0))
+
+def init_salinity(data, coords, l0):
+    init_concentration(data, coords, l0)
+    data[0][...] = 1.0 - data[0][...]
+
+def compute(args):
+    from hysop import Field, Box, Simulation, Problem, MPIParams, IOParams, vprint, \
+                      ScalarParameter
+    from hysop.defaults import VelocityField, VorticityField, \
+                               DensityField, ViscosityField, \
+                               LevelSetField, PenalizationField, \
+                               EnstrophyParameter, TimeParameters, \
+                               VolumicIntegrationParameter
+    from hysop.constants import Implementation, AdvectionCriteria, \
+                                BoxBoundaryCondition, BoundaryCondition, \
+                                Backend
+
+    from hysop.operators import DirectionalAdvection, DirectionalStretching,       \
+                                Diffusion, ComputeMeanField,                       \
+                                PoissonCurl, AdaptiveTimeStep,               \
+                                Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
+                                ParameterPlotter, Integrate, HDF_Writer,           \
+                                CustomSymbolicOperator, DirectionalSymbolic
+
+    from hysop.methods import SpaceDiscretization, Remesh, TimeIntegrator, \
+                              ComputeGranularity, Interpolation
+    
+    from hysop.symbolic import sm, space_symbols, local_indices_symbols
+    from hysop.symbolic.base import SymbolicTensor
+    from hysop.symbolic.field import curl
+    from hysop.symbolic.relational import Assignment, LogicalLE, LogicalGE
+    from hysop.symbolic.misc import Select
+    from hysop.symbolic.tmp import TmpScalar
+    from hysop.tools.string_utils import framed_str
+
+    # Constants
+    l0 = 1.5 #initial thickness of the profile
+    dim = args.ndim
+    if (dim==2):
+        (Sc, tau, Vp, Rs, Xo, Xn, N) = (0.70,  25, 0.04, 2.0, (-600,0),   (600,750), (1537, 512))
+    elif (dim==3):
+        (Sc, tau, Vp, Rs, Xo, Xn, N) = (7.00, 25, 0.04, 2.0, (-110,0,0), (65,100,100), (1537, 512, 512))
+    else:
+        raise NotImplementedError
+
+    nu_S = ScalarParameter(name='nu_S', dtype=args.dtype, const=True, initial_value=1.0/Sc)
+    nu_C = ScalarParameter(name='nu_C', dtype=args.dtype, const=True, initial_value=1.0/(tau*Sc))
+    nu_W = ScalarParameter(name='nu_W', dtype=args.dtype, const=True, initial_value=1.0)
+
+    # Define the domain
+    npts=N[::-1]
+    Xo=Xo[::-1]
+    Xn=Xn[::-1]
+    
+    lboundaries = (BoxBoundaryCondition.PERIODIC,)*(dim-1)+(BoxBoundaryCondition.SYMMETRIC,)
+    rboundaries = (BoxBoundaryCondition.PERIODIC,)*(dim-1)+(BoxBoundaryCondition.SYMMETRIC,)
+
+    S_lboundaries = (BoundaryCondition.PERIODIC,)*(dim-1)+(BoundaryCondition.HOMOGENEOUS_NEUMANN,)
+    S_rboundaries = (BoundaryCondition.PERIODIC,)*(dim-1)+(BoundaryCondition.HOMOGENEOUS_DIRICHLET,)
+    C_lboundaries = (BoundaryCondition.PERIODIC,)*(dim-1)+(BoundaryCondition.HOMOGENEOUS_DIRICHLET,)
+    C_rboundaries = (BoundaryCondition.PERIODIC,)*(dim-1)+(BoundaryCondition.HOMOGENEOUS_NEUMANN,)
+
+    box = Box(origin=Xo, length=np.subtract(Xn,Xo),
+                lboundaries=lboundaries, rboundaries=rboundaries)
+    
+    # Get default MPI Parameters from domain (even for serial jobs)
+    mpi_params = MPIParams(comm=box.task_comm,
+                           task_id=box.current_task())
+
+    # Setup usual implementation specific variables
+    impl = args.impl
+    enforce_implementation = args.enforce_implementation
+    extra_op_kwds = {'mpi_params': mpi_params}
+    if (impl is Implementation.PYTHON):
+        method = {}
+    elif (impl is Implementation.OPENCL):
+        # For the OpenCL implementation we need to setup the compute device
+        # and configure how the code is generated and compiled at runtime.
+                
+        # Create an explicit OpenCL context from user parameters
+        from hysop.backend.device.opencl.opencl_tools import get_or_create_opencl_env
+        cl_env = get_or_create_opencl_env(mpi_params=mpi_params, 
+                                          platform_id=args.cl_platform_id, 
+                                          device_id=args.cl_device_id)
+        
+        # Configure OpenCL kernel generation and tuning (already done by HysopArgParser)
+        from hysop.methods import OpenClKernelConfig
+        method = { OpenClKernelConfig: args.opencl_kernel_config }
+        
+        # Setup opencl specific extra operator keyword arguments
+        extra_op_kwds['cl_env'] = cl_env
+    else:
+        msg='Unknown implementation \'{}\'.'.format(impl)
+        raise ValueError(msg)
+
+    # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
+    t, dt = TimeParameters(dtype=args.dtype)
+    velo  = VelocityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
+    C = Field(domain=box, name='C', dtype=args.dtype, lboundaries=C_lboundaries, rboundaries=C_rboundaries)
+    S = Field(domain=box, name='S', dtype=args.dtype, lboundaries=S_lboundaries, rboundaries=S_rboundaries)
+    
+    # Symbolic fields
+    frame = velo.domain.frame
+    Us    = velo.s(*frame.vars)
+    Ws    = vorti.s(*frame.vars)
+    Cs    = C.s(*frame.vars)
+    Ss    = S.s(*frame.vars)
+    dts   = dt.s
+
+    ### Build the directional operators
+    #> Directional advection 
+    advec = DirectionalAdvection(implementation=impl,
+            name='advec',
+            velocity = velo,       
+            advected_fields = (vorti,S),
+            velocity_cfl = args.cfl,
+            variables = {velo: npts, vorti: npts, S: npts},
+            dt=dt, **extra_op_kwds)
+   
+    V0 = [0]*dim
+    VP = [0]*dim
+    VP[0] = Vp
+    advec_C = DirectionalAdvection(implementation=impl,
+            name='advec_C',
+            velocity = velo,       
+            advected_fields = (C,),
+            relative_velocity = VP,
+            velocity_cfl = args.cfl,
+            variables = {velo: npts, C: npts},
+            dt=dt, **extra_op_kwds)
+    
+    #> Stretch vorticity
+    if (dim==3):
+        stretch = DirectionalStretching(implementation=impl,
+                 name='stretch',
+                 pretty_name='stretch',
+                 formulation = args.stretching_formulation,
+                 velocity  = velo,       
+                 vorticity = vorti,
+                 variables = {velo: npts, vorti: npts},
+                 dt=dt, **extra_op_kwds)
+    elif (dim==2):
+        stretch = None 
+    else:
+        msg='Unsupported dimension {}.'.format(dim)
+        raise RuntimeError(msg)
+    
+    #> Diffusion of vorticity, S and C
+    diffuse_S = Diffusion(implementation=impl,
+             enforce_implementation=enforce_implementation,
+             name='diffuse_S',
+             pretty_name='diffS',
+             nu = nu_S,
+             Fin = S,
+             variables = {S: npts},
+             dt=dt, 
+             **extra_op_kwds)
+    diffuse_C = Diffusion(implementation=impl,
+             enforce_implementation=enforce_implementation,
+             name='diffuse_C',
+             pretty_name='diffC',
+             nu = nu_C,
+             Fin = C,
+             variables = {C: npts},
+             dt=dt, **extra_op_kwds)
+
+    #> External force rot(-rho*g) = rot(Rs*S + C)
+    Fext = np.zeros(shape=(dim,), dtype=object).view(SymbolicTensor)
+    fext = -(Rs*Ss + Cs)
+    Fext[0] = fext
+    lhs = Ws.diff(frame.time)
+    rhs = curl(Fext, frame)
+    exprs = Assignment.assign(lhs, rhs)
+    external_force = DirectionalSymbolic(name='Fext', 
+                                    implementation=impl,
+                                    exprs=exprs, dt=dt,
+                                    variables={vorti: npts,
+                                               S: npts,
+                                               C: npts},
+                                    **extra_op_kwds)
+    
+    splitting = StrangSplitting(splitting_dim=dim, 
+                    order=args.strang_order)
+    splitting.push_operators(advec, advec_C, stretch, external_force)
+    
+    ### Build standard operators
+    #> Poisson operator to recover the velocity from the vorticity
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
+                            variables={velo:npts, vorti: npts},
+                            diffusion=nu_W, dt=dt,
+                            implementation=impl,
+                            enforce_implementation=enforce_implementation,
+                            **extra_op_kwds)
+    
+    #> Operator to compute the infinite norm of the velocity
+    min_max_U = MinMaxFieldStatistics(name='min_max_U', field=velo,
+            Finf=True, implementation=impl, variables={velo:npts},
+            **extra_op_kwds)
+    #> Operator to compute the infinite norm of the vorticity
+    min_max_W = MinMaxFieldStatistics(field=vorti,
+            Finf=True, implementation=impl, variables={vorti:npts},
+            **extra_op_kwds)
+    
+    #> Operators to dump all fields
+    io_params = IOParams(filename='fields', frequency=args.dump_freq)
+    dump_fields = HDF_Writer(name='dump',
+                             io_params=io_params,
+                             force_backend=Backend.OPENCL,
+                             variables={velo: npts, 
+                                        vorti: npts,
+                                        C: npts, 
+                                        S: npts},
+                             **extra_op_kwds)
+
+    #> Operator to compute and save mean fields
+    axes = list(range(0, dim-1))
+    view = [slice(None,None,None),]*dim
+    view[-1] = (-200.0,+200.0)
+    view = tuple(view)
+    io_params = IOParams(filename='horizontally_averaged_profiles', frequency=0)
+    compute_mean_fields = ComputeMeanField(name='mean', 
+            fields={C: (view, axes), S: (view, axes)},
+            variables={C: npts, S: npts},
+            io_params=io_params)
+
+    ### Adaptive timestep operator
+    adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True,
+                                    name='merge_dt', pretty_name='dt', )
+    dt_cfl = adapt_dt.push_cfl_criteria(cfl=args.cfl, 
+                                        Fmin=min_max_U.Fmin,
+                                        Fmax=min_max_U.Fmax,
+                                        equivalent_CFL=True, 
+                                        relative_velocities=[V0, VP],
+                                        name='dt_cfl', pretty_name='CFL')
+    dt_advec = adapt_dt.push_advection_criteria(lcfl=args.lcfl, Finf=min_max_W.Finf,
+                                                 criteria=AdvectionCriteria.W_INF,
+                                                 name='dt_lcfl', pretty_name='LCFL')
+
+    
+    ## Create the problem we want to solve and insert our 
+    # directional splitting subgraph and the standard operators.
+    # The method dictionnary passed to this graph will be dispatched
+    # accross all operators contained in the graph.
+    method.update(
+            { 
+               ComputeGranularity:    args.compute_granularity,
+               SpaceDiscretization:   args.fd_order,
+               TimeIntegrator:        args.time_integrator,
+               Remesh:                args.remesh_kernel,
+               Interpolation:         args.interpolation
+            }
+        )
+
+    problem = Problem(method=method)
+    problem.insert(poisson, 
+                   diffuse_S, diffuse_C,
+                   splitting, 
+                   dump_fields,
+                   compute_mean_fields,
+                   min_max_U, min_max_W, 
+                   adapt_dt)
+    problem.build(args)
+    
+    # If a visu_rank was provided, and show_graph was set,
+    # display the graph on the given process rank.
+    if args.display_graph:
+        problem.display()
+    
+    # Create a simulation
+    # (do not forget to specify the t and dt parameters here)
+    simu = Simulation(start=args.tstart, end=args.tend, 
+                      nb_iter=args.nb_iter,
+                      max_iter=args.max_iter,
+                      dt0=args.dt, times_of_interest=args.dump_times,
+                      t=t, dt=dt)
+    simu.write_parameters(t, dt_cfl, dt_advec, dt, 
+            min_max_U.Finf, min_max_W.Finf, adapt_dt.equivalent_CFL,
+            filename='parameters.txt', precision=8)
+    
+    # Initialize vorticity, velocity, S and C on all topologies
+    problem.initialize_field(field=velo,  formula=init_velocity)
+    problem.initialize_field(field=vorti, formula=init_vorticity)
+    problem.initialize_field(field=C,     formula=init_concentration, l0=l0)
+    problem.initialize_field(field=S,     formula=init_salinity, l0=l0)
+    
+    # Finally solve the problem 
+    problem.solve(simu, dry_run=args.dry_run)
+    
+    # Finalize
+    problem.finalize()
+
+
+if __name__=='__main__':
+    from examples.example_utils import HysopArgParser, colors
+
+    class ParticleAboveSaltArgParser(HysopArgParser):
+        def __init__(self):
+            prog_name = 'particle_above_salt_bc'
+            default_dump_dir = '{}/hysop_examples/{}'.format(HysopArgParser.tmp_dir(), 
+                    prog_name)
+
+            description=colors.color('HySoP Particles Above Salt Example: ', fg='blue',
+                    style='bold')
+            description+=colors.color('[Meiburg 2014]', fg='yellow', style='bold')
+            description+=colors.color('\nSediment-laden fresh water above salt water.', 
+                    fg='yellow')
+            description+='\n'
+            description+='\nThis example focuses on a validation study for the '
+            description+='hybrid particle-mesh vortex method in the Boussinesq approximation.'
+    
+            super(ParticleAboveSaltArgParser, self).__init__(
+                 prog_name=prog_name,
+                 description=description,
+                 default_dump_dir=default_dump_dir)
+
+        def _setup_parameters(self, args):
+            dim = args.ndim
+            if (dim not in (2,3)):
+                msg='Domain should be 2D or 3D.'
+                self.error(msg)
+
+
+    parser = ParticleAboveSaltArgParser()
+
+    parser.set_defaults(impl='cl', ndim=2, npts=(64,),
+                        box_origin=(0.0,), box_length=(1.0,), 
+                        tstart=0.0, tend=500.0, 
+                        dt=1e-6, cfl=4.00, lcfl=0.95,
+                        dump_times=tuple(float(x) for x in range(0,500,10)),
+                        dump_freq=0)
+
+    parser.run(compute)
+
diff --git a/examples/particles_above_salt/particles_above_salt_bc_3d.py b/examples/particles_above_salt/particles_above_salt_bc_3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..f16898e6140c41e070f9776cab058b7a0fe62a52
--- /dev/null
+++ b/examples/particles_above_salt/particles_above_salt_bc_3d.py
@@ -0,0 +1,359 @@
+## See Meiburg 2012 & 2014
+## Sediment-laden fresh water above salt water.
+    
+import numpy as np
+import scipy as sp
+import sympy as sm
+
+# initialize vorticity
+def init_vorticity(data, coords, component=None):
+    # the flow is initially quiescent
+    for d in data:
+        d[...] = 0.0
+
+# initialize velocity
+def init_velocity(data, coords, component=None):
+    # the flow is initially quiescent
+    for d in data:
+        d[...] = 0.0
+
+# initialize sediment concentration and salinity
+def delta(*coords):
+    d = np.prod(*coords)
+    return np.zeros_like(d)
+
+def delta(Ys, l0):
+    Y0 = 1
+    for Yi in Ys:
+        Y0 = Y0*Yi
+    return 0.1*l0*(np.random.rand(*Y0.shape)-0.5)
+    
+def init_concentration(data, coords, l0):
+    coords, = coords
+    X = coords[0]
+    Ys = coords[0:]
+    data[0][...] = 0.5*(1.0 +
+            sp.special.erf((X-delta(Ys,l0))/l0))
+
+def init_salinity(data, coords, l0):
+    init_concentration(data, coords, l0)
+    data[0][...] = 1.0 - data[0][...]
+
+def compute(args):
+    from hysop import Field, Box, Simulation, Problem, MPIParams, IOParams, vprint, \
+                      ScalarParameter
+    from hysop.defaults import VelocityField, VorticityField, \
+                               DensityField, ViscosityField, \
+                               LevelSetField, PenalizationField, \
+                               EnstrophyParameter, TimeParameters, \
+                               VolumicIntegrationParameter
+    from hysop.constants import Implementation, AdvectionCriteria, \
+                                BoxBoundaryCondition, BoundaryCondition, \
+                                Backend
+
+    from hysop.operators import DirectionalAdvection, DirectionalStretching,       \
+                                Diffusion, ComputeMeanField,                       \
+                                PoissonCurl, AdaptiveTimeStep,               \
+                                Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
+                                ParameterPlotter, Integrate, HDF_Writer,           \
+                                CustomSymbolicOperator, DirectionalSymbolic
+
+    from hysop.methods import SpaceDiscretization, Remesh, TimeIntegrator, \
+                              ComputeGranularity, Interpolation
+    
+    from hysop.symbolic import sm, space_symbols, local_indices_symbols
+    from hysop.symbolic.base import SymbolicTensor
+    from hysop.symbolic.field import curl
+    from hysop.symbolic.relational import Assignment, LogicalLE, LogicalGE
+    from hysop.symbolic.misc import Select
+    from hysop.symbolic.tmp import TmpScalar
+    from hysop.tools.string_utils import framed_str
+
+    # Constants
+    l0 = 1.5 #initial thickness of the profile
+    dim = args.ndim
+    if (dim==2):
+        (Sc, tau, Vp, Rs, Xo, Xn, N) = (0.70,  25, 0.04, 2.0, (-600,0), (600,750), (1537, 512))
+    elif (dim==3):
+        (Sc, tau, Vp, Rs, Xo, Xn, N) = (7.00, 25, 0.04, 2.0, (-110,0,0), (65,100,100), (3073, 1024, 1024))
+    else:
+        raise NotImplementedError
+
+    nu_S = ScalarParameter(name='nu_S', dtype=args.dtype, const=True, initial_value=1.0/Sc)
+    nu_C = ScalarParameter(name='nu_C', dtype=args.dtype, const=True, initial_value=1.0/(tau*Sc))
+    nu_W = ScalarParameter(name='nu_W', dtype=args.dtype, const=True, initial_value=1.0)
+
+    # Define the domain
+    npts=N[::-1]
+    Xo=Xo[::-1]
+    Xn=Xn[::-1]
+    
+    lboundaries = (BoxBoundaryCondition.PERIODIC,)*(dim-1)+(BoxBoundaryCondition.SYMMETRIC,)
+    rboundaries = (BoxBoundaryCondition.PERIODIC,)*(dim-1)+(BoxBoundaryCondition.SYMMETRIC,)
+
+    S_lboundaries = (BoundaryCondition.PERIODIC,)*(dim-1)+(BoundaryCondition.HOMOGENEOUS_NEUMANN,)
+    S_rboundaries = (BoundaryCondition.PERIODIC,)*(dim-1)+(BoundaryCondition.HOMOGENEOUS_DIRICHLET,)
+    C_lboundaries = (BoundaryCondition.PERIODIC,)*(dim-1)+(BoundaryCondition.HOMOGENEOUS_DIRICHLET,)
+    C_rboundaries = (BoundaryCondition.PERIODIC,)*(dim-1)+(BoundaryCondition.HOMOGENEOUS_NEUMANN,)
+
+    box = Box(origin=Xo, length=np.subtract(Xn,Xo),
+                lboundaries=lboundaries, rboundaries=rboundaries)
+    
+    # Get default MPI Parameters from domain (even for serial jobs)
+    mpi_params = MPIParams(comm=box.task_comm,
+                           task_id=box.current_task())
+
+    # Setup usual implementation specific variables
+    impl = args.impl
+    enforce_implementation = args.enforce_implementation
+
+    extra_op_kwds = {'mpi_params': mpi_params}
+    if (impl is Implementation.PYTHON):
+        method = {}
+    elif (impl is Implementation.OPENCL):
+        # For the OpenCL implementation we need to setup the compute device
+        # and configure how the code is generated and compiled at runtime.
+                
+        # Create an explicit OpenCL context from user parameters
+        from hysop.backend.device.opencl.opencl_tools import get_or_create_opencl_env
+        cl_env = get_or_create_opencl_env(mpi_params=mpi_params, 
+                                          platform_id=args.cl_platform_id, 
+                                          device_id=args.cl_device_id)
+        
+        # Configure OpenCL kernel generation and tuning (already done by HysopArgParser)
+        from hysop.methods import OpenClKernelConfig
+        method = { OpenClKernelConfig: args.opencl_kernel_config }
+        
+        # Setup opencl specific extra operator keyword arguments
+        extra_op_kwds['cl_env'] = cl_env
+    else:
+        msg='Unknown implementation \'{}\'.'.format(impl)
+        raise ValueError(msg)
+
+    # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
+    t, dt = TimeParameters(dtype=args.dtype)
+    velo  = VelocityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
+    C = Field(domain=box, name='C', dtype=args.dtype, lboundaries=C_lboundaries, rboundaries=C_rboundaries)
+    S = Field(domain=box, name='S', dtype=args.dtype, lboundaries=S_lboundaries, rboundaries=S_rboundaries)
+    
+    # Symbolic fields
+    frame = velo.domain.frame
+    Us    = velo.s(*frame.vars)
+    Ws    = vorti.s(*frame.vars)
+    Cs    = C.s(*frame.vars)
+    Ss    = S.s(*frame.vars)
+    dts   = dt.s
+
+    ### Build the directional operators
+    #> Directional advection 
+    advec = DirectionalAdvection(implementation=impl,
+            name='advec',
+            velocity = velo,       
+            advected_fields = (vorti,S),
+            velocity_cfl = args.cfl,
+            variables = {velo: npts, vorti: npts, S: npts},
+            dt=dt, **extra_op_kwds)
+   
+    V0 = [0]*dim
+    VP = [0]*dim
+    VP[0] = Vp
+    advec_C = DirectionalAdvection(implementation=impl,
+            name='advec_C',
+            velocity = velo,       
+            advected_fields = (C,),
+            relative_velocity = VP,
+            velocity_cfl = args.cfl,
+            variables = {velo: npts, C: npts},
+            dt=dt, **extra_op_kwds)
+    
+    #> Stretch vorticity
+    if (dim==3):
+        stretch = DirectionalStretching(implementation=impl,
+                 name='stretch',
+                 pretty_name='stretch',
+                 formulation = args.stretching_formulation,
+                 velocity  = velo,       
+                 vorticity = vorti,
+                 variables = {velo: npts, vorti: npts},
+                 dt=dt, **extra_op_kwds)
+    elif (dim==2):
+        stretch = None 
+    else:
+        msg='Unsupported dimension {}.'.format(dim)
+        raise RuntimeError(msg)
+    
+    #> Diffusion of vorticity, S and C
+    diffuse_S = Diffusion(implementation=impl,
+             enforce_implementation=enforce_implementation,
+             name='diffuse_S',
+             pretty_name='diffS',
+             nu = nu_S,
+             Fin = S,
+             variables = {S: npts},
+             dt=dt, **extra_op_kwds)
+    diffuse_C = Diffusion(implementation=impl,
+             enforce_implementation=enforce_implementation,
+             name='diffuse_C',
+             pretty_name='diffC',
+             nu = nu_C,
+             Fin = C,
+             variables = {C: npts},
+             dt=dt, **extra_op_kwds)
+
+    #> External force rot(-rho*g) = rot(Rs*S + C)
+    Fext = np.zeros(shape=(dim,), dtype=object).view(SymbolicTensor)
+    fext = -(Rs*Ss + Cs)
+    Fext[0] = fext
+    lhs = Ws.diff(frame.time)
+    rhs = curl(Fext, frame)
+    exprs = Assignment.assign(lhs, rhs)
+    external_force = DirectionalSymbolic(name='Fext', 
+                                    implementation=impl,
+                                    exprs=exprs, dt=dt,
+                                    variables={vorti: npts,
+                                               S: npts,
+                                               C: npts},
+                                    **extra_op_kwds)
+    
+    splitting = StrangSplitting(splitting_dim=dim, 
+                    order=args.strang_order)
+    splitting.push_operators(advec, advec_C, stretch, external_force)
+    
+    ### Build standard operators
+    #> Poisson operator to recover the velocity from the vorticity
+    poisson = PoissonCurl(name='poisson',
+                          velocity=velo, vorticity=vorti, 
+                          variables={velo:npts, vorti: npts},
+                          diffusion=nu_W, dt=dt,
+                          implementation=impl, 
+                          enforce_implementation=enforce_implementation,
+                          **extra_op_kwds)
+    
+    #> Operator to compute the infinite norm of the velocity
+    min_max_U = MinMaxFieldStatistics(name='min_max_U', field=velo,
+            Finf=True, implementation=impl, variables={velo:npts},
+            **extra_op_kwds)
+    #> Operator to compute the infinite norm of the vorticity
+    min_max_W = MinMaxFieldStatistics(field=vorti,
+            Finf=True, implementation=impl, variables={vorti:npts},
+            **extra_op_kwds)
+    
+    #> Operators to dump all fields
+    io_params = IOParams(filename='fields', frequency=args.dump_freq)
+    dump_fields = HDF_Writer(name='dump',
+                             io_params=io_params,
+                             force_backend=Backend.OPENCL,
+                             variables={vorti: npts,
+                                        velo: npts, 
+                                        C: npts, 
+                                        S: npts},
+                             **extra_op_kwds)
+
+    ### Adaptive timestep operator
+    adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True,
+                                    name='merge_dt', pretty_name='dt',
+                                    max_dt=5.0)
+    dt_cfl = adapt_dt.push_cfl_criteria(cfl=args.cfl, 
+                                        Fmin=min_max_U.Fmin,
+                                        Fmax=min_max_U.Fmax,
+                                        equivalent_CFL=True, 
+                                        relative_velocities=[V0, VP],
+                                        name='dt_cfl', pretty_name='CFL')
+    dt_advec = adapt_dt.push_advection_criteria(lcfl=args.lcfl, Finf=min_max_W.Finf,
+                                                 criteria=AdvectionCriteria.W_INF,
+                                                 name='dt_lcfl', pretty_name='LCFL')
+
+    
+    ## Create the problem we want to solve and insert our 
+    # directional splitting subgraph and the standard operators.
+    # The method dictionnary passed to this graph will be dispatched
+    # accross all operators contained in the graph.
+    method.update(
+            { 
+               ComputeGranularity:    args.compute_granularity,
+               SpaceDiscretization:   args.fd_order,
+               TimeIntegrator:        args.time_integrator,
+               Remesh:                args.remesh_kernel,
+               Interpolation:         args.interpolation
+            }
+        )
+
+    problem = Problem(method=method)
+    problem.insert(poisson, 
+                   diffuse_S, diffuse_C,
+                   dump_fields,
+                   splitting, 
+                   min_max_U, min_max_W, adapt_dt)
+    problem.build(args)
+    
+    # If a visu_rank was provided, and show_graph was set,
+    # display the graph on the given process rank.
+    if args.display_graph:
+        problem.display()
+    
+    # Create a simulation
+    # (do not forget to specify the t and dt parameters here)
+    simu = Simulation(start=args.tstart, end=args.tend, 
+                      nb_iter=args.nb_iter,
+                      max_iter=args.max_iter,
+                      dt0=args.dt, times_of_interest=args.dump_times,
+                      t=t, dt=dt)
+    simu.write_parameters(t, dt_cfl, dt_advec, dt, 
+            min_max_U.Finf, min_max_W.Finf, adapt_dt.equivalent_CFL,
+            filename='parameters.txt', precision=8)
+    
+    # Initialize vorticity, velocity, S and C on all topologies
+    problem.initialize_field(field=velo,  formula=init_velocity)
+    problem.initialize_field(field=vorti, formula=init_vorticity)
+    problem.initialize_field(field=C,     formula=init_concentration, l0=l0)
+    problem.initialize_field(field=S,     formula=init_salinity, l0=l0)
+    
+    # Finally solve the problem 
+    problem.solve(simu, dry_run=args.dry_run)
+    
+    # Finalize
+    problem.finalize()
+
+
+if __name__=='__main__':
+    from examples.example_utils import HysopArgParser, colors
+
+    class ParticleAboveSaltArgParser(HysopArgParser):
+        def __init__(self):
+            prog_name = 'particle_above_salt_bc_3d'
+            default_dump_dir = '{}/hysop_examples/{}'.format(HysopArgParser.tmp_dir(), 
+                    prog_name)
+
+            description=colors.color('HySoP Particles Above Salt Example: ', fg='blue',
+                    style='bold')
+            description+=colors.color('[Meiburg 2014]', fg='yellow', style='bold')
+            description+=colors.color('\nSediment-laden fresh water above salt water.', 
+                    fg='yellow')
+            description+='\n'
+            description+='\nThis example focuses on a validation study for the '
+            description+='hybrid particle-mesh vortex method in the Boussinesq approximation.'
+    
+            super(ParticleAboveSaltArgParser, self).__init__(
+                 prog_name=prog_name,
+                 description=description,
+                 default_dump_dir=default_dump_dir)
+
+        def _setup_parameters(self, args):
+            dim = args.ndim
+            if (dim not in (2,3)):
+                msg='Domain should be 2D or 3D.'
+                self.error(msg)
+
+
+    parser = ParticleAboveSaltArgParser()
+
+    parser.set_defaults(impl='cl', ndim=3, npts=(64,),
+                        box_origin=(0.0,), box_length=(1.0,), 
+                        tstart=0.0, tend=201.0, 
+                        dt=1e-6, cfl=12.00, lcfl=0.95,
+                        dump_times=(25.0, 50.0, 75.0, 100.0, 125.0, 150.0, 175.0, 200.0),
+                        dump_freq=0)
+
+    parser.run(compute)
+
diff --git a/examples/particles_above_salt/particles_above_salt_periodic.py b/examples/particles_above_salt/particles_above_salt_periodic.py
index f1d3b34225d00bc04c58026410fd51f3d102193d..e90f8a67b039e192943202132f973d5e315f5382 100644
--- a/examples/particles_above_salt/particles_above_salt_periodic.py
+++ b/examples/particles_above_salt/particles_above_salt_periodic.py
@@ -47,17 +47,18 @@ def init_lambda(data, coords):
     data[0][...] *= 1.0e8
 
 def compute(args):
-    from hysop import Field, Box, Simulation, Problem, MPIParams, IOParams, vprint
+    from hysop import Field, Box, Simulation, Problem, MPIParams, IOParams, vprint, \
+                      ScalarParameter
     from hysop.defaults import VelocityField, VorticityField, \
                                DensityField, ViscosityField, \
                                LevelSetField, PenalizationField, \
                                EnstrophyParameter, TimeParameters, \
                                VolumicIntegrationParameter
-    from hysop.constants import Implementation, AdvectionCriteria
+    from hysop.constants import Implementation, AdvectionCriteria, BoxBoundaryCondition
 
     from hysop.operators import DirectionalAdvection, DirectionalDiffusion,        \
                                 DirectionalStretchingDiffusion,                    \
-                                PoissonRotational, AdaptiveTimeStep,               \
+                                PoissonCurl, AdaptiveTimeStep,               \
                                 Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
                                 ParameterPlotter, Integrate, HDF_Writer,           \
                                 DirectionalSymbolic, ComputeMeanField
@@ -75,8 +76,7 @@ def compute(args):
 
     # Constants
     l0 = 1.5 #initial thickness of the profile
-    (Sc, tau, Vp, Rs, Xo, Xn, N) = (0.70,  25, 0.04, 2.0, (-600,0),   (600,750),    (1537, 487))
-    #(Sc, tau, Vp, Rs, Xo, Xn, N) = (7.00,  25, 0.04, 2.0, (-110,0,0), (65,100,100), (1537, 512, 512))
+    (Sc, tau, Vp, Rs, Xo, Xn, N) = (0.70,  25, 0.04, 2.0, (-600,0),   (600,750),    (1536, 512))
 
     nu_S = 1.0/Sc
     nu_C = 1.0/(tau*Sc)
@@ -118,10 +118,10 @@ def compute(args):
     # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
     t, dt = TimeParameters(dtype=args.dtype)
     velo  = VelocityField(domain=box, dtype=args.dtype)
-    vorti = VorticityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
+    C = Field(domain=box, name='C', dtype=args.dtype)
+    S = Field(domain=box, name='S', dtype=args.dtype)
     _lambda = PenalizationField(domain=box, dtype=args.dtype)
-    C     = Field(domain=box, name='C', dtype=args.dtype)
-    S     = Field(domain=box, name='S', dtype=args.dtype)
     
     # Symbolic fields
     frame = velo.domain.frame
@@ -129,8 +129,8 @@ def compute(args):
     Ws    = vorti.s(*frame.vars)
     Cs    = C.s(*frame.vars)
     Ss    = S.s(*frame.vars)
-    lambdas = _lambda.s(*frame.vars)
     dts   = dt.s
+    lambdas = _lambda.s(*frame.vars)
 
     ### Build the directional operators
     #> Directional penalization
@@ -152,8 +152,9 @@ def compute(args):
             advected_fields = (vorti,S),
             velocity_cfl = args.cfl,
             variables = {velo: npts, vorti: npts, S: npts},
-            dt=dt)
-    
+            dt=dt, **extra_op_kwds)
+   
+    V0 = [0]*dim
     VP = [0]*dim
     VP[-1] = Vp
     advec_C = DirectionalAdvection(implementation=impl,
@@ -163,7 +164,7 @@ def compute(args):
             relative_velocity = VP,
             velocity_cfl = args.cfl,
             variables = {velo: npts, C: npts},
-            dt=dt)
+            dt=dt, **extra_op_kwds)
     
     #> Stretch and diffuse vorticity
     if (dim==3):
@@ -222,12 +223,13 @@ def compute(args):
     splitting = StrangSplitting(splitting_dim=dim, 
                     order=args.strang_order)
     splitting.push_operators(penalization, advec, advec_C, stretch_diffuse, 
-                                            diffuse_S, diffuse_C, external_force)
+                                diffuse_S, diffuse_C, external_force)
     
     ### Build standard operators
     #> Poisson operator to recover the velocity from the vorticity
-    poisson = PoissonRotational(name='poisson', velocity=velo, vorticity=vorti, 
-                            variables={velo:npts, vorti: npts})
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
+                            variables={velo:npts, vorti: npts},
+                            implementation=impl, **extra_op_kwds)
     
     #> Operator to compute the infinite norm of the velocity
     min_max_U = MinMaxFieldStatistics(name='min_max_U', field=velo,
@@ -272,9 +274,12 @@ def compute(args):
 
     adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True, max_dt=max_dt,
                                     name='merge_dt', pretty_name='dt', )
-    dt_cfl = adapt_dt.push_cfl_criteria(cfl=args.cfl, Finf=min_max_U.Finf,
-                                            equivalent_CFL=True, 
-                                            name='dt_cfl', pretty_name='CFL')
+    dt_cfl = adapt_dt.push_cfl_criteria(cfl=args.cfl, 
+                                        Fmin=min_max_U.Fmin,
+                                        Fmax=min_max_U.Fmax,
+                                        equivalent_CFL=True, 
+                                        relative_velocities=[V0, VP],
+                                        name='dt_cfl', pretty_name='CFL')
     dt_advec = adapt_dt.push_advection_criteria(lcfl=args.lcfl, Finf=min_max_W.Finf,
                                                  criteria=AdvectionCriteria.W_INF,
                                                  name='dt_lcfl', pretty_name='LCFL')
@@ -301,7 +306,7 @@ def compute(args):
                    compute_mean_fields,
                    min_max_U, min_max_W, 
                    adapt_dt)
-    problem.build()
+    problem.build(args)
     
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
@@ -365,7 +370,7 @@ if __name__=='__main__':
 
     parser = ParticleAboveSaltArgParser()
 
-    parser.set_defaults(impl='cl', ndim=2, npts=(65,),
+    parser.set_defaults(impl='cl', ndim=2, npts=(64,),
                         box_origin=(0.0,), box_length=(1.0,), 
                         tstart=0.0, tend=500.0, 
                         dt=1e-6, cfl=0.5, lcfl=0.125,
diff --git a/examples/particles_above_salt/particles_above_salt_symmetrized.py b/examples/particles_above_salt/particles_above_salt_symmetrized.py
index 53afffa57b0222509c8bc808ff6016f1fb2a9453..d48a421ee7e3855c070043dc882a05c394b890c8 100644
--- a/examples/particles_above_salt/particles_above_salt_symmetrized.py
+++ b/examples/particles_above_salt/particles_above_salt_symmetrized.py
@@ -31,27 +31,29 @@ def delta(Ys, l0):
 def init_concentration(data, coords, l0):
     coords, = coords
     X = coords[-1].copy()
-    X = np.abs(X-1200.0) - 600.0
+    Xs = np.sign(X-1200.0)
+    Xa = np.abs(X-1200.0) - 600.0
     Ys = coords[:-1]
     data[0][...] = 0.5*(1.0 +
-            sp.special.erf((X-delta(Ys,l0))/l0))
+            sp.special.erf((Xa-delta(Ys,l0))/l0))
 
 def init_salinity(data, coords, l0):
     init_concentration(data, coords, l0)
     data[0][...] = 1.0 - data[0][...]
 
 def compute(args):
-    from hysop import Field, Box, Simulation, Problem, MPIParams, IOParams, vprint
+    from hysop import Field, Box, Simulation, Problem, MPIParams, IOParams, vprint, \
+                      ScalarParameter
     from hysop.defaults import VelocityField, VorticityField, \
                                DensityField, ViscosityField, \
                                LevelSetField, PenalizationField, \
                                EnstrophyParameter, TimeParameters, \
                                VolumicIntegrationParameter
-    from hysop.constants import Implementation, AdvectionCriteria
+    from hysop.constants import Implementation, AdvectionCriteria, BoxBoundaryCondition
 
-    from hysop.operators import DirectionalAdvection, DirectionalDiffusion,        \
-                                DirectionalStretchingDiffusion,                    \
-                                PoissonRotational, AdaptiveTimeStep,               \
+    from hysop.operators import DirectionalAdvection, DirectionalStretching,       \
+                                Diffusion,                                         \
+                                PoissonCurl, AdaptiveTimeStep,               \
                                 Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
                                 ParameterPlotter, Integrate, HDF_Writer,           \
                                 DirectionalSymbolic, ComputeMeanField
@@ -69,15 +71,16 @@ def compute(args):
 
     # Constants
     l0 = 1.5 #initial thickness of the profile
-    (Sc, tau, Vp, Rs, Xo, Xn, N) = (0.70,  25, 0.04, 2.0, (-600,0),   (600,750),    (1537, 487))
+    (Sc, tau, Vp, Rs, Xo, Xn, N) = (0.70,  25, 0.04, 2.0, (-600,0),   (600,750),    (1537, 512))
     #(Sc, tau, Vp, Rs, Xo, Xn, N) = (7.00,  25, 0.04, 2.0, (-110,0,0), (65,100,100), (1537, 512, 512))
 
-    nu_S = 1.0/Sc
-    nu_C = 1.0/(tau*Sc)
+    nu_S = ScalarParameter(name='nu_S', dtype=args.dtype, const=True, initial_value=1.0/Sc)
+    nu_C = ScalarParameter(name='nu_C', dtype=args.dtype, const=True, initial_value=1.0/(tau*Sc))
+    nu_W = ScalarParameter(name='nu_W', dtype=args.dtype, cosnt=True, initial_value=1.0)
 
     # Define the domain
     dim = args.ndim
-    npts = (3073,513)
+    npts = (2048,8192)
     Xo = (0,0)
     Xn = (2400,750)
     box = Box(origin=Xo, length=np.subtract(Xn,Xo))
@@ -89,7 +92,9 @@ def compute(args):
     # Setup usual implementation specific variables
     impl = args.impl
     extra_op_kwds = {'mpi_params': mpi_params}
-    if (impl is Implementation.OPENCL):
+    if (impl is Implementation.PYTHON):
+        method = {}
+    elif (impl is Implementation.OPENCL):
         # For the OpenCL implementation we need to setup the compute device
         # and configure how the code is generated and compiled at runtime.
                 
@@ -98,6 +103,7 @@ def compute(args):
         cl_env = get_or_create_opencl_env(mpi_params=mpi_params, 
                                           platform_id=args.cl_platform_id, 
                                           device_id=args.cl_device_id)
+
         tg = cl_env.build_typegen(args.dtype, 'dec', False, False)
         
         # Configure OpenCL kernel generation and tuning (already done by HysopArgParser)
@@ -113,9 +119,9 @@ def compute(args):
     # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
     t, dt = TimeParameters(dtype=args.dtype)
     velo  = VelocityField(domain=box, dtype=args.dtype)
-    vorti = VorticityField(domain=box, dtype=args.dtype)
-    C     = Field(domain=box, name='C', dtype=args.dtype)
-    S     = Field(domain=box, name='S', dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
+    C = Field(domain=box, name='C', dtype=args.dtype)
+    S = Field(domain=box, name='S', dtype=args.dtype)
     
     # Symbolic fields
     frame = velo.domain.frame
@@ -141,6 +147,12 @@ def compute(args):
                                              tg.dump(-Vp),
                                              'int' if tg.fbtype=='float' else 'long',
                                              tg.dump(1200.0))
+    
+    V0  = [0]*dim
+    pVP = [0]*dim
+    mVP = [0]*dim
+    pVP[-1] = +Vp
+    mVP[-1] = -Vp
 
     advec_C = DirectionalAdvection(implementation=impl,
             name='advec_C',
@@ -151,42 +163,42 @@ def compute(args):
             variables = {velo: npts, C: npts},
             dt=dt, **extra_op_kwds)
     
-    #> Stretch and diffuse vorticity
+    #> Stretch vorticity
     if (dim==3):
-        stretch_diffuse = DirectionalStretchingDiffusion(implementation=impl,
-                 name='stretch_diffuse',
-                 pretty_name='sdiff',
+        stretch = DirectionalStretching(implementation=impl,
+                 name='stretch',
+                 pretty_name='stretch',
                  formulation = args.stretching_formulation,
-                 viscosity = 1.0,
                  velocity  = velo,       
                  vorticity = vorti,
                  variables = {velo: npts, vorti: npts},
                  dt=dt, **extra_op_kwds)
     elif (dim==2):
-        stretch_diffuse = DirectionalDiffusion(implementation=impl,
-                 name='diffuse_{}'.format(vorti.name),
-                 pretty_name=u'diff{}'.format(vorti.pretty_name.decode('utf-8')),
-                 coeffs = 1.0,
-                 fields  = vorti,
-                 variables = {vorti: npts},
-                 dt=dt, **extra_op_kwds)
+        stretch = None 
     else:
         msg='Unsupported dimension {}.'.format(dim)
         raise RuntimeError(msg)
     
-    #> Diffusion of S and C
-    diffuse_S = DirectionalDiffusion(implementation=impl,
+    #> Diffusion of vorticity, S and C
+    diffuse_W = Diffusion(implementation=impl,
+             name='diffuse_{}'.format(vorti.name),
+             pretty_name=u'diff{}'.format(vorti.pretty_name.decode('utf-8')),
+             nu = nu_W,
+             Fin = vorti,
+             variables = {vorti: npts},
+             dt=dt, **extra_op_kwds)
+    diffuse_S = Diffusion(implementation=impl,
              name='diffuse_S',
              pretty_name='diffS',
-             coeffs = nu_S,
-             fields = S,
+             nu = nu_S,
+             Fin = S,
              variables = {S: npts},
              dt=dt, **extra_op_kwds)
-    diffuse_C = DirectionalDiffusion(implementation=impl,
+    diffuse_C = Diffusion(implementation=impl,
              name='diffuse_C',
              pretty_name='diffC',
-             coeffs = nu_C,
-             fields = C,
+             nu = nu_C,
+             Fin = C,
              variables = {C: npts},
              dt=dt, **extra_op_kwds)
 
@@ -210,12 +222,11 @@ def compute(args):
     
     splitting = StrangSplitting(splitting_dim=dim, 
                     order=args.strang_order)
-    splitting.push_operators(advec, advec_C, stretch_diffuse,
-                                diffuse_S, diffuse_C, external_force)
+    splitting.push_operators(advec, advec_C, stretch, external_force)
     
     ### Build standard operators
     #> Poisson operator to recover the velocity from the vorticity
-    poisson = PoissonRotational(name='poisson', velocity=velo, vorticity=vorti, 
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
                             variables={velo:npts, vorti: npts})
     
     #> Operator to compute the infinite norm of the velocity
@@ -248,21 +259,14 @@ def compute(args):
             io_params=io_params)
 
     ### Adaptive timestep operator
-    dx = np.min(np.divide(box.length, np.asarray(npts)-1))
-    S_dt = 0.5*(dx**2)/nu_S
-    C_dt = 0.5*(dx**2)/nu_C
-    W_dt = 0.5*(dx**2)/1.0
-    msg = 'S_dt={}, C_dt={}, W_dt={}'
-    msg = msg.format(S_dt, C_dt, W_dt)
-    msg = '\n'+framed_str(' DIFFUSION STABILITY CRITERIA ', msg)
-    vprint(msg)
-    max_dt = min(S_dt, C_dt, W_dt)
-
-    adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True, max_dt=max_dt,
+    adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True,
                                     name='merge_dt', pretty_name='dt', )
-    dt_cfl = adapt_dt.push_cfl_criteria(cfl=args.cfl, Finf=min_max_U.Finf,
-                                            equivalent_CFL=True, 
-                                            name='dt_cfl', pretty_name='CFL')
+    dt_cfl = adapt_dt.push_cfl_criteria(cfl=args.cfl, 
+                                        Fmin=min_max_U.Fmin,
+                                        Fmax=min_max_U.Fmax,
+                                        equivalent_CFL=True, 
+                                        relative_velocities=[V0, pVP, mVP],
+                                        name='dt_cfl', pretty_name='CFL')
     dt_advec = adapt_dt.push_advection_criteria(lcfl=args.lcfl, Finf=min_max_W.Finf,
                                                  criteria=AdvectionCriteria.W_INF,
                                                  name='dt_lcfl', pretty_name='LCFL')
@@ -284,12 +288,13 @@ def compute(args):
 
     problem = Problem(method=method)
     problem.insert(poisson, 
+                   diffuse_W, diffuse_S, diffuse_C,
                    splitting, 
                    dump_fields,
                    compute_mean_fields,
                    min_max_U, min_max_W, 
                    adapt_dt)
-    problem.build()
+    problem.build(args)
     
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
@@ -329,11 +334,11 @@ if __name__=='__main__':
             default_dump_dir = '{}/hysop_examples/{}'.format(HysopArgParser.tmp_dir(), 
                     prog_name)
 
-            description=colors.color('HySoP Particles Above Salt Example: ', fg='blue', 
-                                        style='bold')
+            description=colors.color('HySoP Particles Above Salt Example: ', fg='blue',
+                    style='bold')
             description+=colors.color('[Meiburg 2014]', fg='yellow', style='bold')
             description+=colors.color('\nSediment-laden fresh water above salt water.', 
-                                        fg='yellow')
+                    fg='yellow')
             description+='\n'
             description+='\nThis example focuses on a validation study for the '
             description+='hybrid particle-mesh vortex method in the Boussinesq approximation.'
@@ -352,7 +357,7 @@ if __name__=='__main__':
 
     parser = ParticleAboveSaltArgParser()
 
-    parser.set_defaults(impl='cl', ndim=2, npts=(65,),
+    parser.set_defaults(impl='cl', ndim=2, npts=(64,),
                         box_origin=(0.0,), box_length=(1.0,), 
                         tstart=0.0, tend=500.0, 
                         dt=1e-6, cfl=0.5, lcfl=0.125,
diff --git a/examples/scalar_advection/levelset.py b/examples/scalar_advection/levelset.py
index 5b538131926bcd278c622711ee1f250fe45867e5..9639474272296a41bf47101a5e1ecc6464a31a8b 100644
--- a/examples/scalar_advection/levelset.py
+++ b/examples/scalar_advection/levelset.py
@@ -5,7 +5,7 @@ import sympy as sm
 
 
 from hysop import Field, Box, Simulation, Problem, \
-                  ScalarParameter, MPIParams, Discretization, CartesianTopology
+                  ScalarParameter, MPIParams, CartesianDiscretization, CartesianTopology
 from hysop.constants import Implementation, Backend
 from hysop.operators import DirectionalAdvection, StrangSplitting, Integrate, \
     AnalyticField, Advection
@@ -31,8 +31,8 @@ def init_scalar(data, coords):
     data[0][rr < 0.1] += 1.
 
 # Define domain
-npts = (65,)*dim
-npts_s = (65, )*dim
+npts   = (64,)*dim
+npts_s = (64, )*dim
 box  = Box(origin=(0.,)*dim, length=(1.,)*dim, dim=dim)
 if dim == 3:
     dt0 = 0.35 / (4. * pi)
@@ -68,7 +68,7 @@ simu.write_parameters(simu.t, vol, filename='volume.txt', precision=8)
 
 
 # ghosts = (2,)*dim
-# d3d    = Discretization(npts, ghosts)
+# d3d    = CartesianDiscretization(npts, ghosts, default_boundaries=True)
 # topo = CartesianTopology(domain=box, discretization=d3d, backend=Backend.OPENCL)
 
 # Setup implementation specific variables
diff --git a/examples/scalar_advection/scalar_advection.py b/examples/scalar_advection/scalar_advection.py
index 26cbbc1cf0912b0e07de5f6950db14cbe293c174..11d007844752144bca59f586e06e0f98d85749af 100644
--- a/examples/scalar_advection/scalar_advection.py
+++ b/examples/scalar_advection/scalar_advection.py
@@ -109,8 +109,8 @@ def compute(args):
         problem.insert(splitting)
     
     # Add a writer of input field at given frequency.
-    problem.dump_inputs(fields=scalar, filename='S0', frequency=args.dump_freq)
-    problem.build()
+    problem.dump_inputs(fields=scalar, filename='S0', frequency=args.dump_freq, **extra_op_kwds)
+    problem.build(args)
 
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
diff --git a/examples/scalar_advection/turbulent_scalar_advection.py b/examples/scalar_advection/turbulent_scalar_advection.py
index 8a887f383980912a58101db5b737c5eaacf9572b..1c8664a819b0ccd2b8924786a2fc196f9d0db426 100644
--- a/examples/scalar_advection/turbulent_scalar_advection.py
+++ b/examples/scalar_advection/turbulent_scalar_advection.py
@@ -60,13 +60,13 @@ from hysop.constants import Implementation, AdvectionCriteria, HYSOP_REAL, \
     StretchingFormulation
 from hysop.topology.cartesian_topology import CartesianTopology
 from hysop.operators import Advection, StaticDirectionalStretching, Diffusion, \
-                            PoissonRotational, AdaptiveTimeStep, DirectionalDiffusion, \
+                            PoissonCurl, AdaptiveTimeStep, DirectionalDiffusion, \
                             Enstrophy, MinMaxFieldStatistics, StrangSplitting,    \
                             ParameterPlotter, DirectionalAdvection
 from hysop.numerics.odesolvers.runge_kutta import RK2
 from hysop.methods import SpaceDiscretization, Remesh, TimeIntegrator, \
                           ComputeGranularity, Interpolation, StrangOrder
-from hysop.tools.parameters import Discretization
+from hysop.tools.parameters import CartesianDiscretization
 
 # Define the domain
 dim  = 3
@@ -98,14 +98,15 @@ method = {}
 # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
 t, dt = TimeParameters(dtype=HYSOP_REAL)
 velo = VelocityField(domain=box, dtype=HYSOP_REAL)
-vorti = VorticityField(domain=box, dtype=HYSOP_REAL)
+vorti = VorticityField(velocity=velo, dtype=HYSOP_REAL)
 enstrophy = EnstrophyParameter(dtype=HYSOP_REAL)
 wdotw = Field(domain=box, dtype=HYSOP_REAL, is_vector=False, name="WdotW")
 scal = Field(domain=box, name='Scalar', is_vector=False)
 
 # Topologies
 topo_nogh = CartesianTopology(domain=box,
-                              discretization=Discretization(npts_uw),
+                              discretization=CartesianDiscretization(npts_uw,
+                                  default_boundaries=True),
                               mpi_params=mpi_params,
                               cutdirs=[False, False, True])
 
@@ -155,12 +156,12 @@ splitting.push_operators(stretch)
 diffuse = Diffusion(
     implementation=Implementation.FORTRAN,
     name='diffuse',
-    viscosity=VISCOSITY,
+    nu=VISCOSITY,
     Fin=vorti,
     variables={vorti: topo_nogh},
     dt=dt, **extra_op_kwds)
 #> Poisson operator to recover the velocity from the vorticity
-poisson = PoissonRotational(
+poisson = PoissonCurl(
     implementation=Implementation.FORTRAN,
     name='poisson',
     velocity=velo,
diff --git a/examples/scalar_diffusion/scalar_diffusion.py b/examples/scalar_diffusion/scalar_diffusion.py
index 62ea8c52d178dd57b1e94273f02bc4100019f5e3..4ee0f75ce85c69e4d1907eb8e9f6d87386209400 100755
--- a/examples/scalar_diffusion/scalar_diffusion.py
+++ b/examples/scalar_diffusion/scalar_diffusion.py
@@ -80,8 +80,8 @@ def compute(args):
     # Add a writer of input field at given frequency.
     problem = Problem(method=method)
     problem.insert(splitting)
-    problem.dump_inputs(fields=scalar, filename='S0', frequency=args.dump_freq)
-    problem.build()
+    problem.dump_inputs(fields=scalar, filename='S0', frequency=args.dump_freq, **extra_op_kwds)
+    problem.build(args)
     
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
diff --git a/examples/sediment_deposit/C_IN.DAT b/examples/sediment_deposit/C_IN.DAT
new file mode 100644
index 0000000000000000000000000000000000000000..f8412dd4e2de0ab1db796efabc684d2fc0220855
--- /dev/null
+++ b/examples/sediment_deposit/C_IN.DAT
@@ -0,0 +1,24 @@
+-nombre de points pour flow
+256
+-nombre de points pour densite
+256
+-n1 d'iterations
+100000
+- viscositea dans grains
+0.01
+- viscositea dans fluide
+0.01
+-flux
+0.
+- drho 
+0.2
+- rayon des grains 
+0.08
+- nombre de grains dans chaque direction 
+6
+- tau surface tension
+0.
+- Prandtl
+0.00001
+- tstop
+100.
diff --git a/examples/sediment_deposit/init.f90 b/examples/sediment_deposit/init.f90
new file mode 100644
index 0000000000000000000000000000000000000000..2732943655b4daa455027fa1ced192e7dcab202a
--- /dev/null
+++ b/examples/sediment_deposit/init.f90
@@ -0,0 +1,64 @@
+	subroutine init(xp1,yp1,om,npart)
+
+
+        include 'param.i'
+        include 'param.h'
+        include 'arrays.h'
+
+      dimension xp1(*),yp1(*)
+      dimension om(*)
+	dimension xb(npg,npg),yb(npg,npg)
+
+	pi2=2.*3.1415926
+	ampl=0.05
+        eps=dx2
+
+	alambda=float(nb)
+
+
+	pi=3.1415926
+	spi=sqrt(pi)
+	pi2=2.*pi
+
+	npart=0
+        do i=1,nx1
+        do j=1,ny1
+        xx=(float(i)-1.)*dx1
+        yy=(float(j)-1.)*dx1
+	strength=0.
+	omg(i,j)=strength
+	vxg(i,j)=0.
+	vyg(i,j)=0.
+	strg1(i,j)=0.
+	strg2(i,j)=0.
+        npart=npart+1
+        xp1(npart)=xx
+        yp1(npart)=yy
+        om(npart)=strength
+        enddo
+        enddo
+
+	ugmax=-1.
+	ugmin=1.
+c	ug > 0 dans les grains, <0 entre les grains
+        do j=1,ny2
+        yy=abs(float(j-1)*dx2)*alambda
+	phase=rand()
+	phase=0.
+        do i=1,nx2
+        xx=(abs(float(i-1)*dx2)+phase)*alambda
+        yy=abs(float(j-1)*dx2)*alambda
+	ug(i,j)=sin(pi2*xx)*sin(pi2*yy)
+c	cas ou on advecte la densite
+c        ug(i,j)=0.+
+c     1           0.5*drho*(1.+tanh(ug(i,j)/(eps)))
+	ug_init(i,j)=ug(i,j)
+	ugmax=amax1(ugmax,ug(i,j))
+	ugmin=amin1(ugmin,ug(i,j))
+	enddo
+	enddo
+
+	print*,'ugmin et ugmax a init ',ugmin,ugmax
+
+	return
+	end
diff --git a/examples/sediment_deposit/sediment_deposit.py b/examples/sediment_deposit/sediment_deposit.py
new file mode 100644
index 0000000000000000000000000000000000000000..c8a3c880d15cdbeb666405c54d00281389b7199b
--- /dev/null
+++ b/examples/sediment_deposit/sediment_deposit.py
@@ -0,0 +1,374 @@
+import numpy as np
+import scipy as sp
+import sympy as sm
+import numba as nb
+
+TANK_RATIO      = 3
+SEDIMENT_COUNT  = 2048*TANK_RATIO
+SEDIMENT_RADIUS = 0.5e-2
+DISCRETIZATION  = 512
+
+# initialize vorticity
+def init_vorticity(data, coords, component=None):
+    # the flow is initially quiescent
+    for d in data:
+        d[...] = 0.0
+
+# initialize velocity
+def init_velocity(data, coords, component=None):
+    # the flow is initially quiescent
+    for d in data:
+        d[...] = 0.0
+
+def init_sediment(data, coords, nblobs, rblob):
+    from hysop import vprint
+    data   = data[0]
+    coords = coords[0]
+    X, Y = coords
+    R2 = rblob * rblob
+    
+    cache_file='/tmp/C_init_{}_{}'.format('_'.join(str(x) for x in data.shape),
+            str(abs(hash((TANK_RATIO,nblobs,rblob)))))
+    try:
+        D = np.load(file=cache_file+'.npz')
+        vprint('  *Initializing sediments from cache: "{}.npz".'.format(cache_file))
+        data[...] = D['data']
+    except:
+        X,   Y   = X.ravel(), Y.ravel()
+        dx,  dy  = X[1]-X[0], Y[1]-Y[0]
+        Nx,  Ny  = X.size, Y.size
+        Rx,  Ry  = 2+int(rblob/dx), 2+int(rblob/dy)
+        assert (rblob>=dx), 'Sediment radius < dx.'
+        assert (rblob>=dy), 'Sediment radius < dy.'
+        
+        Bx = 1*np.random.rand(nblobs)
+        By = 1*np.random.rand(nblobs)
+        Ix = np.floor(Bx/dx).astype(np.int32)
+        Iy = np.floor(By/dy).astype(np.int32)
+        Px = Bx - Ix*dx
+        Py = By - Iy*dy
+        
+        from hysop.tools.numba_utils import make_numba_signature
+        args = (Ix, Iy, Bx, By, data)
+        signature, _ = make_numba_signature(*args)
+         
+        @nb.guvectorize([signature],
+            '(n),(n),(n),(n),(n0,n1)', 
+            target='parallel',
+            nopython=True, cache=True)
+        def iter_blobs(Ix, Iy, Bx, By, data):
+            for k in xrange(nblobs):
+                #print 'blob {}/{}'.format(k+1, nblobs)
+                ix, iy = Ix[k], Iy[k]
+                px, py = Px[k], Py[k]
+                for i in xrange(-Ry, +Ry):
+                    ii = iy+i
+                    if (ii<0) or (ii>=Ny):
+                        continue
+                    dy2 = (py + i*dy)**2
+                    for j in xrange(-Rx, +Rx):
+                        jj = ix+j
+                        if (jj<0) or (jj>=Nx):
+                            continue
+                        dx2 = (px - j*dx)**2
+                        d = dx2 + dy2
+                        if (d<R2):
+                            data[ii,jj] = 0.5
+
+        vprint('  *Initializing sediments of radius {} with {} random blobs.'.format(rblob, nblobs))
+        data[...]  = 0.0
+        iter_blobs(*args)
+
+        # we cache initialization
+        np.savez_compressed(file=cache_file, data=data)
+        vprint('  *Caching data to "{}.npz".'.format(cache_file))
+
+
+
+def compute(args):
+    from hysop import Field, Box, Simulation, Problem, MPIParams, IOParams, vprint, \
+                      ScalarParameter
+    from hysop.defaults import VelocityField, VorticityField, \
+                               DensityField, ViscosityField, \
+                               LevelSetField, PenalizationField, \
+                               EnstrophyParameter, TimeParameters, \
+                               VolumicIntegrationParameter
+    from hysop.constants import Implementation, AdvectionCriteria, \
+                                BoxBoundaryCondition, BoundaryCondition, \
+                                Backend
+
+    from hysop.operators import DirectionalAdvection, DirectionalStretching,       \
+                                Diffusion, ComputeMeanField,                       \
+                                PoissonCurl, AdaptiveTimeStep,                     \
+                                Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
+                                ParameterPlotter, Integrate, HDF_Writer,           \
+                                CustomSymbolicOperator, DirectionalSymbolic,       \
+                                SpectralExternalForce, SymbolicExternalForce
+
+    from hysop.methods import SpaceDiscretization, Remesh, TimeIntegrator, \
+                              ComputeGranularity, Interpolation
+    
+    from hysop.numerics.odesolvers.runge_kutta import Euler, RK2, RK3, RK4
+    from hysop.symbolic import sm, space_symbols, local_indices_symbols
+    from hysop.symbolic.base import SymbolicTensor
+    from hysop.symbolic.field import curl
+    from hysop.symbolic.relational import Assignment, LogicalLE, LogicalGE, LogicalLT, LogicalGT
+    from hysop.symbolic.misc import Select
+    from hysop.symbolic.tmp import TmpScalar
+    from hysop.tools.string_utils import framed_str
+
+    # Constants
+    dim = args.ndim
+    if (dim==2):
+        Xo = (0.0,)*dim
+        Xn = (float(TANK_RATIO), 1.0)
+        nblobs = SEDIMENT_COUNT
+        rblob  = SEDIMENT_RADIUS
+        npts = args.npts
+    else:
+        msg='The {}D has not been implemented yet.'.format(dim)
+        raise NotImplementedError(msg)
+
+    nu_S = ScalarParameter(name='nu_S', dtype=args.dtype, const=True, initial_value=1e-10)
+    nu_W = ScalarParameter(name='nu_W', dtype=args.dtype, const=True, initial_value=1e-2)
+
+    lboundaries = (BoxBoundaryCondition.SYMMETRIC, BoxBoundaryCondition.SYMMETRIC)
+    rboundaries = (BoxBoundaryCondition.SYMMETRIC, BoxBoundaryCondition.SYMMETRIC)
+
+    S_lboundaries = (BoundaryCondition.HOMOGENEOUS_NEUMANN, BoundaryCondition.HOMOGENEOUS_NEUMANN)
+    S_rboundaries = (BoundaryCondition.HOMOGENEOUS_NEUMANN, BoundaryCondition.HOMOGENEOUS_NEUMANN)
+
+    box = Box(origin=Xo, length=np.subtract(Xn,Xo),
+                lboundaries=lboundaries, rboundaries=rboundaries)
+    
+    # Get default MPI Parameters from domain (even for serial jobs)
+    mpi_params = MPIParams(comm=box.task_comm,
+                           task_id=box.current_task())
+
+    # Setup usual implementation specific variables
+    impl = args.impl
+    enforce_implementation = args.enforce_implementation
+    extra_op_kwds = {'mpi_params': mpi_params}
+    if (impl is Implementation.PYTHON):
+        method = {}
+    elif (impl is Implementation.OPENCL):
+        # For the OpenCL implementation we need to setup the compute device
+        # and configure how the code is generated and compiled at runtime.
+                
+        # Create an explicit OpenCL context from user parameters
+        from hysop.backend.device.opencl.opencl_tools import get_or_create_opencl_env
+        cl_env = get_or_create_opencl_env(mpi_params=mpi_params, 
+                                          platform_id=args.cl_platform_id, 
+                                          device_id=args.cl_device_id)
+        
+        # Configure OpenCL kernel generation and tuning (already done by HysopArgParser)
+        from hysop.methods import OpenClKernelConfig
+        method = { OpenClKernelConfig: args.opencl_kernel_config }
+        
+        # Setup opencl specific extra operator keyword arguments
+        extra_op_kwds['cl_env'] = cl_env
+    else:
+        msg='Unknown implementation \'{}\'.'.format(impl)
+        raise ValueError(msg)
+
+    # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
+    t, dt = TimeParameters(dtype=args.dtype)
+    velo  = VelocityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
+    S = Field(domain=box, name='S', dtype=args.dtype)
+            #lboundaries=S_lboundaries, rboundaries=S_rboundaries)
+    
+    # Symbolic fields
+    frame = velo.domain.frame
+    Us    = velo.s(*frame.vars)
+    Ws    = vorti.s(*frame.vars)
+    Ss    = S.s(*frame.vars)
+    dts   = dt.s
+
+    ### Build the directional operators
+    #> Directional advection 
+    advec = DirectionalAdvection(implementation=impl,
+            name='advec',
+            velocity = velo,       
+            advected_fields = (vorti, S),
+            velocity_cfl = args.cfl,
+            variables = {velo: npts, 
+                         vorti: npts, 
+                         S: npts},
+            dt=dt, **extra_op_kwds)
+   
+    #> Stretch vorticity
+    if (dim==3):
+        stretch = DirectionalStretching(implementation=impl,
+                 name='stretch',
+                 pretty_name='stretch',
+                 formulation = args.stretching_formulation,
+                 velocity  = velo,       
+                 vorticity = vorti,
+                 variables = {velo: npts, vorti: npts},
+                 dt=dt, **extra_op_kwds)
+    elif (dim==2):
+        stretch = None 
+    else:
+        msg='Unsupported dimension {}.'.format(dim)
+        raise RuntimeError(msg)
+    
+    splitting = StrangSplitting(splitting_dim=dim, 
+                    order=args.strang_order)
+    splitting.push_operators(advec, stretch)
+    
+    ### Build standard operators
+    #> Poisson operator to recover the velocity from the vorticity
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
+                            variables={velo:npts, vorti: npts},
+                            diffusion=nu_W, dt=dt,
+                            implementation=impl,
+                            enforce_implementation=enforce_implementation,
+                            **extra_op_kwds)
+
+    #> External force rot(-rho*g) = rot(-(1+S)) = rot(-S)
+    g = 9.81
+    Fext = SymbolicExternalForce(name='S', Fext=(0,-g*Ss),
+                                   diffusion = {S: nu_S})
+    external_force = SpectralExternalForce(name='Fext', 
+                                   vorticity=vorti, dt=dt,
+                                   Fext=Fext, Finf=True,
+                                   implementation=impl,
+                                   variables={vorti: npts, S: npts},
+                                   **extra_op_kwds)
+    
+    #> Operator to compute the infinite norm of the velocity
+    min_max_U = MinMaxFieldStatistics(name='min_max_U', field=velo,
+            Finf=True, implementation=impl, variables={velo:npts},
+            **extra_op_kwds)
+    #> Operator to compute the infinite norm of the vorticity
+    min_max_W = MinMaxFieldStatistics(field=vorti,
+            Finf=True, implementation=impl, variables={vorti:npts},
+            **extra_op_kwds)
+    
+    #> Operators to dump all fields
+    io_params = IOParams(filename='fields', frequency=args.dump_freq)
+    dump_fields = HDF_Writer(name='dump',
+                             io_params=io_params,
+                             force_backend=Backend.OPENCL,
+                             variables={velo: npts, 
+                                        vorti: npts,
+                                        S: npts},
+                             **extra_op_kwds)
+
+    #> Operator to compute and save mean fields
+    axes = list(range(1, dim))
+    view = [slice(None,None,None),]*dim
+    view = tuple(view)
+    io_params = IOParams(filename='horizontally_averaged_profiles', frequency=0)
+    compute_mean_fields = ComputeMeanField(name='mean', 
+            fields={S: (view, axes)},
+            variables={S: npts},
+            io_params=io_params)
+
+    ### Adaptive timestep operator
+    adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True,
+                                    name='merge_dt', pretty_name='dt')
+    dt_cfl = adapt_dt.push_cfl_criteria(cfl=args.cfl, 
+                                        Finf=min_max_U.Finf,
+                                        equivalent_CFL=True, 
+                                        name='dt_cfl', pretty_name='CFL')
+    dt_advec = adapt_dt.push_advection_criteria(lcfl=args.lcfl, Finf=min_max_W.Finf,
+                                                 criteria=AdvectionCriteria.W_INF,
+                                                 name='dt_lcfl', pretty_name='LCFL')
+    dt_force = adapt_dt.push_cst_criteria(cst=10000,
+                                        Finf=external_force.Finf,
+                                        name='dt_force', pretty_name='FEXT')
+
+    
+    ## Create the problem we want to solve and insert our 
+    # directional splitting subgraph and the standard operators.
+    # The method dictionnary passed to this graph will be dispatched
+    # accross all operators contained in the graph.
+    method.update(
+            { 
+               ComputeGranularity:    args.compute_granularity,
+               SpaceDiscretization:   args.fd_order,
+               TimeIntegrator:        args.time_integrator,
+               Remesh:                args.remesh_kernel,
+               Interpolation:         args.interpolation
+            }
+        )
+
+    problem = Problem(method=method)
+    problem.insert(poisson, 
+                   dump_fields,
+                   min_max_U, min_max_W, adapt_dt,
+                   splitting, 
+                   compute_mean_fields,
+                   external_force)
+    problem.build(args)
+    
+    # If a visu_rank was provided, and show_graph was set,
+    # display the graph on the given process rank.
+    if args.display_graph:
+        problem.display()
+    
+    # Create a simulation
+    # (do not forget to specify the t and dt parameters here)
+    simu = Simulation(start=args.tstart, end=args.tend, 
+                      nb_iter=args.nb_iter,
+                      max_iter=args.max_iter,
+                      dt0=args.dt, times_of_interest=args.dump_times,
+                      t=t, dt=dt)
+    simu.write_parameters(t, dt_cfl, dt_advec, dt, 
+            min_max_U.Finf, min_max_W.Finf, adapt_dt.equivalent_CFL,
+            filename='parameters.txt', precision=8)
+    
+    # Initialize vorticity, velocity, S on all topologies
+    problem.initialize_field(field=velo,  formula=init_velocity)
+    problem.initialize_field(field=vorti, formula=init_vorticity)
+    problem.initialize_field(field=S,     formula=init_sediment, 
+            nblobs=nblobs, rblob=rblob, without_ghosts=True)
+    
+    # Finally solve the problem 
+    problem.solve(simu, dry_run=args.dry_run)
+    
+    # Finalize
+    problem.finalize()
+
+
+if __name__=='__main__':
+    from examples.example_utils import HysopArgParser, colors
+
+    class ParticleAboveSaltArgParser(HysopArgParser):
+        def __init__(self):
+            prog_name = 'sediment_deposit'
+            default_dump_dir = '{}/hysop_examples/{}'.format(HysopArgParser.tmp_dir(), 
+                    prog_name)
+
+            description=colors.color('HySoP Sediment Deposit Example: ', fg='blue',
+                    style='bold')
+            description+='\n'
+            description+='\nThis example focuses on a validation study for the '
+            description+='hybrid particle-mesh vortex method for sediment deposit.'
+    
+            super(ParticleAboveSaltArgParser, self).__init__(
+                 prog_name=prog_name,
+                 description=description,
+                 default_dump_dir=default_dump_dir)
+
+        def _setup_parameters(self, args):
+            dim = args.ndim
+            if (dim not in (2,3)):
+                msg='Domain should be 2D or 3D.'
+                self.error(msg)
+
+
+    parser = ParticleAboveSaltArgParser()
+
+    parser.set_defaults(impl='cl', ndim=2,
+                        npts=(TANK_RATIO*DISCRETIZATION+1,DISCRETIZATION+1),
+                        box_origin=(0.0,), box_length=(1.0,), 
+                        tstart=0.0, tend=20.0,
+                        dt=1e-6, cfl=32.0, lcfl=0.90,
+                        #dump_times=tuple(float(x) for x in range(0,100000,1000)),
+                        dump_freq=10)
+
+    parser.run(compute)
+
diff --git a/examples/sediment_deposit/sediment_deposit_levelset.py b/examples/sediment_deposit/sediment_deposit_levelset.py
new file mode 100644
index 0000000000000000000000000000000000000000..2aadf45faf24f4848145d101d9acd78aba28e65e
--- /dev/null
+++ b/examples/sediment_deposit/sediment_deposit_levelset.py
@@ -0,0 +1,438 @@
+import numpy as np
+import scipy as sp
+import sympy as sm
+import numba as nb
+import skfmm
+
+TANK_RATIO      = 1
+FILL_PCT        = 0.25
+SEDIMENT_RADIUS = 5e-3
+SEDIMENT_COUNT  = int(1.15*(FILL_PCT*TANK_RATIO / (np.pi*SEDIMENT_RADIUS**2)))
+DISCRETIZATION  = 256
+
+BLOB_INIT = False
+NB = 5
+
+# initialize vorticity
+def init_vorticity(data, coords, component=None):
+    # the flow is initially quiescent
+    for d in data:
+        d[...] = 0.0
+
+# initialize velocity
+def init_velocity(data, coords, component=None):
+    # the flow is initially quiescent
+    for d in data:
+        d[...] = 0.0
+
+def init_phi(data, coords, nblobs, rblob):
+    from hysop import vprint
+    data   = data[0]
+    coords = coords[0]
+    X, Y = coords
+    Bx = np.random.rand(nblobs)
+    By = TANK_RATIO*np.random.rand(nblobs)
+    R2 = rblob * rblob
+    
+    cache_file='/tmp/C_init_ls_{}_{}'.format('_'.join(str(x) for x in data.shape),
+            str(abs(hash((BLOB_INIT, NB, TANK_RATIO, nblobs, rblob)))))
+    try:
+        D = np.load(file=cache_file+'.npz')
+        vprint('  *Initializing sediments from cache: "{}.npz".'.format(cache_file))
+        data[...] = D['data']
+    except:
+
+        if not BLOB_INIT:
+            vprint('  *Initializing sediments with sines...')
+            data[...] = np.sin(2*np.pi*NB*X)*np.sin(2*np.pi*NB*Y)
+            data[...] = np.abs(data)
+            return
+
+        # we cache initialization
+        vprint('  *Initializing sediments of radius {} with {} random blobs.'.format(rblob, nblobs))
+        np.savez_compressed(file=cache_file, data=data)
+        vprint('  *Caching data to "{}.npz".'.format(cache_file))
+        
+        X,   Y   = X.ravel(), Y.ravel()
+        dx,  dy  = X[1]-X[0], Y[1]-Y[0]
+        Nx,  Ny  = X.size, Y.size
+        Rx,  Ry  = 2*(int(rblob/dx)+1), 2*(int(rblob/dy)+1)
+        assert (rblob>=dx), 'Sediment radius < dx.'
+        assert (rblob>=dy), 'Sediment radius < dy.'
+        
+        Ix = np.floor(Bx/dx).astype(np.int32)
+        Iy = np.floor(By/dy).astype(np.int32)
+        Px = Bx - Ix*dx
+        Py = By - Iy*dy
+        
+        from hysop.tools.numba_utils import make_numba_signature
+        args = (Ix, Iy, Bx, By, data)
+        signature, _ = make_numba_signature(*args)
+         
+        @nb.guvectorize([signature],
+            '(n),(n),(n),(n),(n0,n1)', 
+            target='parallel',
+            nopython=True, cache=True)
+        def iter_blobs(Ix, Iy, Bx, By, data):
+            for k in xrange(nblobs):
+                #print 'blob {}/{}'.format(k+1, nblobs)
+                ix, iy = Ix[k], Iy[k]
+                px, py = Px[k], Py[k]
+                for i in xrange(-Ry, +Ry):
+                    ii = iy+i
+                    if (ii<0) or (ii>=Ny):
+                        continue
+                    dy2 = (py + i*dy)**2 / R2
+                    for j in xrange(-Rx, +Rx):
+                        jj = ix+j
+                        if (jj<0) or (jj>=Nx):
+                            continue
+                        dx2 = (px - j*dx)**2 / R2
+                        d = dx2 + dy2 - 1
+                        if (d<data[ii,jj]): 
+                            data[ii,jj] = d
+
+        vprint('  *Initializing sediments of radius {} with {} random blobs.'.format(rblob, nblobs))
+        data[...]  = np.inf
+        iter_blobs(*args)
+        data[...] = skfmm.distance(data, dx=(dy,dx))
+
+        # we cache initialization
+        np.savez_compressed(file=cache_file, data=data)
+        vprint('  *Caching data to "{}.npz".'.format(cache_file))
+
+
+
+def compute(args):
+    from hysop import Field, Box, Simulation, Problem, MPIParams, IOParams, vprint, \
+                      ScalarParameter
+    from hysop.defaults import VelocityField, VorticityField, \
+                               DensityField, ViscosityField, \
+                               LevelSetField, PenalizationField, \
+                               EnstrophyParameter, TimeParameters, \
+                               VolumicIntegrationParameter
+    from hysop.constants import Implementation, AdvectionCriteria, \
+                                BoxBoundaryCondition, BoundaryCondition, \
+                                Backend
+
+    from hysop.operators import DirectionalAdvection, DirectionalStretching,       \
+                                Diffusion, ComputeMeanField,                       \
+                                PoissonCurl, AdaptiveTimeStep,                     \
+                                Enstrophy, MinMaxFieldStatistics, StrangSplitting, \
+                                ParameterPlotter, Integrate, HDF_Writer,           \
+                                CustomSymbolicOperator, DirectionalSymbolic,       \
+                                SpectralExternalForce, SymbolicExternalForce
+
+    from hysop.methods import SpaceDiscretization, Remesh, TimeIntegrator, \
+                              ComputeGranularity, Interpolation
+    
+    from hysop.numerics.odesolvers.runge_kutta import Euler, RK2, RK3, RK4
+    from hysop.symbolic import sm, space_symbols, local_indices_symbols
+    from hysop.symbolic.base import SymbolicTensor
+    from hysop.symbolic.field import curl
+    from hysop.symbolic.relational import Assignment, LogicalLE, LogicalGE, LogicalLT, LogicalGT
+    from hysop.symbolic.misc import Select
+    from hysop.symbolic.tmp import TmpScalar
+    from hysop.tools.string_utils import framed_str
+
+    # Constants
+    dim = args.ndim
+    if (dim==2):
+        Xo = (0.0,)*dim
+        Xn = (float(TANK_RATIO), 1.0)
+        nblobs = SEDIMENT_COUNT
+        rblob  = SEDIMENT_RADIUS
+        npts = args.npts
+    else:
+        msg='The {}D has not been implemented yet.'.format(dim)
+        raise NotImplementedError(msg)
+
+    nu_W = ScalarParameter(name='nu_W', dtype=args.dtype, const=True, initial_value=1e-2)
+
+    lboundaries = (BoxBoundaryCondition.SYMMETRIC, BoxBoundaryCondition.SYMMETRIC)
+    rboundaries = (BoxBoundaryCondition.SYMMETRIC, BoxBoundaryCondition.SYMMETRIC)
+
+    S_boundaries = {
+        'lboundaries': (BoundaryCondition.HOMOGENEOUS_NEUMANN, BoundaryCondition.HOMOGENEOUS_NEUMANN),
+        'rboundaries': (BoundaryCondition.HOMOGENEOUS_NEUMANN, BoundaryCondition.HOMOGENEOUS_NEUMANN)
+    }
+
+    box = Box(origin=Xo, length=np.subtract(Xn,Xo),
+                lboundaries=lboundaries, rboundaries=rboundaries)
+    
+    # Get default MPI Parameters from domain (even for serial jobs)
+    mpi_params = MPIParams(comm=box.task_comm,
+                           task_id=box.current_task())
+
+    # Setup usual implementation specific variables
+    impl = args.impl
+    enforce_implementation = args.enforce_implementation
+    extra_op_kwds = {'mpi_params': mpi_params}
+    if (impl is Implementation.PYTHON):
+        method = {}
+    elif (impl is Implementation.OPENCL):
+        # For the OpenCL implementation we need to setup the compute device
+        # and configure how the code is generated and compiled at runtime.
+                
+        # Create an explicit OpenCL context from user parameters
+        from hysop.backend.device.opencl.opencl_tools import get_or_create_opencl_env
+        cl_env = get_or_create_opencl_env(mpi_params=mpi_params, 
+                                          platform_id=args.cl_platform_id, 
+                                          device_id=args.cl_device_id)
+        
+        # Configure OpenCL kernel generation and tuning (already done by HysopArgParser)
+        from hysop.methods import OpenClKernelConfig
+        method = { OpenClKernelConfig: args.opencl_kernel_config }
+        
+        # Setup opencl specific extra operator keyword arguments
+        extra_op_kwds['cl_env'] = cl_env
+    else:
+        msg='Unknown implementation \'{}\'.'.format(impl)
+        raise ValueError(msg)
+
+    # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
+    t, dt = TimeParameters(dtype=args.dtype)
+    velo  = VelocityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
+    phi   = LevelSetField(domain=box, dtype=args.dtype, **S_boundaries)
+    S     = DensityField(name='S', domain=box, dtype=args.dtype, **S_boundaries)
+    Sv    = VolumicIntegrationParameter(field=S)
+    
+    # Symbolic fields
+    frame = velo.domain.frame
+    Us    = velo.s(*frame.vars)
+    Ws    = vorti.s(*frame.vars)
+    phis  = phi.s(*frame.vars)
+    Ss  = S.s(*frame.vars)
+    dts   = dt.s
+
+    ### Build the directional operators
+    #> Directional advection 
+    advec = DirectionalAdvection(implementation=impl,
+            name='advec',
+            velocity = velo,       
+            advected_fields = (vorti, phi),
+            velocity_cfl = args.cfl,
+            variables = {velo:  npts, 
+                         vorti: npts, 
+                         phi:   npts},
+            dt=dt, **extra_op_kwds)
+   
+    #> Recompute density from levelset
+    dx = np.max(np.divide(box.length, np.asarray(args.npts)-1))
+    S1, S2 = 0.5, 0.0
+    pi  = TmpScalar(name='pi',  dtype=args.dtype)
+    eps = TmpScalar(name='eps', dtype=args.dtype)
+    x   = TmpScalar(name='x',   dtype=args.dtype)
+    H   = TmpScalar(name='H',   dtype=args.dtype)
+    smooth_cond = LogicalLT(sm.Abs(x), eps)
+    pos_cond = LogicalGT(x, 0)
+    clamp  = Select(0.0, 1.0, pos_cond)
+    smooth = (x+eps)/(2*eps) + sm.sin(pi*x/eps)/(2*pi)
+    H_eps = Select(clamp, smooth, smooth_cond)
+    #e0 = Assignment(pi, np.pi)
+    #e1 = Assignment(eps, 5*dx)
+    #e2 = Assignment(x, phis*SEDIMENT_RADIUS)
+    #e3 = Assignment(H, H_eps) 
+    #e4 = Assignment(Ss, S1 + (S2-S1)*H)
+    #exprs = (e0,e1,e2,e3,e4)
+    if BLOB_INIT:
+        e = Assignment(Ss, 0.5*LogicalLE(phis, 0))
+    else:
+        e = Assignment(Ss, 0.5*LogicalGT(phis, 0.5))
+        #e = Assignment(Ss, 0.5*LogicalLE(phis, 0))
+    exprs = (e,)
+    eval_fields = DirectionalSymbolic(name='eval_fields', 
+                                    pretty_name=u'{}({})'.format(
+                                        phi.pretty_name.decode('utf-8'), 
+                                        S.pretty_name.decode('utf-8')),
+                                    no_split=True,
+                                    implementation=impl,
+                                    exprs=exprs, dt=dt,
+                                    variables={phi: npts,
+                                               S: npts},
+                                    **extra_op_kwds)
+   
+    #> Stretch vorticity
+    if (dim==3):
+        stretch = DirectionalStretching(implementation=impl,
+                 name='stretch',
+                 pretty_name='stretch',
+                 formulation = args.stretching_formulation,
+                 velocity  = velo,       
+                 vorticity = vorti,
+                 variables = {velo: npts, vorti: npts},
+                 dt=dt, **extra_op_kwds)
+    elif (dim==2):
+        stretch = None 
+    else:
+        msg='Unsupported dimension {}.'.format(dim)
+        raise RuntimeError(msg)
+    
+    #> External force rot(-S*g)
+    Fext = np.zeros(shape=(dim,), dtype=object).view(SymbolicTensor)
+    fext = -Ss
+    Fext[1] = fext
+    lhs = Ws.diff(frame.time)
+    rhs = curl(Fext, frame)
+    exprs = Assignment.assign(lhs, rhs)
+    external_force = DirectionalSymbolic(name='Fext', 
+                                    implementation=impl,
+                                    exprs=exprs, dt=dt,
+                                    variables={vorti: npts,
+                                               S: npts},
+                                    **extra_op_kwds)
+    
+    splitting = StrangSplitting(splitting_dim=dim, 
+                    order=args.strang_order)
+    splitting.push_operators(advec, eval_fields, stretch, external_force)
+    
+    ### Build standard operators
+    #> Poisson operator to recover the velocity from the vorticity
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
+                            variables={velo:npts, vorti: npts},
+                            diffusion=nu_W, dt=dt,
+                            implementation=impl,
+                            enforce_implementation=enforce_implementation,
+                            **extra_op_kwds)
+    
+    #> Operator to compute the infinite norm of the velocity
+    min_max_U = MinMaxFieldStatistics(name='min_max_U', field=velo,
+            Finf=True, implementation=impl, variables={velo:npts},
+            **extra_op_kwds)
+    #> Operator to compute the infinite norm of the vorticity
+    min_max_W = MinMaxFieldStatistics(field=vorti,
+            Finf=True, implementation=impl, variables={vorti:npts},
+            **extra_op_kwds)
+    
+    #> Operators to compute the integrated density
+    integrate_S = Integrate(field=S, variables={S: npts},
+                                    parameter=Sv, scaling='volumic', cst=2,
+                                    implementation=impl, **extra_op_kwds)
+    
+    #> Operators to dump all fields
+    io_params = IOParams(filename='fields', frequency=args.dump_freq)
+    dump_fields = HDF_Writer(name='dump',
+                             io_params=io_params,
+                             force_backend=Backend.OPENCL,
+                             variables={#velo: npts, 
+                                        #vorti: npts,
+                                        phi: npts,
+                                        S: npts},
+                             **extra_op_kwds)
+
+    #> Operator to compute and save mean fields
+    axes = list(range(1, dim))
+    view = [slice(None,None,None),]*dim
+    view = tuple(view)
+    io_params = IOParams(filename='horizontally_averaged_profiles', frequency=0)
+    compute_mean_fields = ComputeMeanField(name='mean', 
+            fields={S: (view, axes)},
+            variables={S: npts},
+            io_params=io_params)
+
+    ### Adaptive timestep operator
+    adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True,
+                                    name='merge_dt', pretty_name='dt',
+                                    max_dt=1e-1)
+    dt_cfl = adapt_dt.push_cfl_criteria(cfl=args.cfl, 
+                                        Finf=min_max_U.Finf,
+                                        equivalent_CFL=True, 
+                                        name='dt_cfl', pretty_name='CFL')
+    dt_advec = adapt_dt.push_advection_criteria(lcfl=args.lcfl, Finf=min_max_W.Finf,
+                                                 criteria=AdvectionCriteria.W_INF,
+                                                 name='dt_lcfl', pretty_name='LCFL')
+
+    
+    ## Create the problem we want to solve and insert our 
+    # directional splitting subgraph and the standard operators.
+    # The method dictionnary passed to this graph will be dispatched
+    # accross all operators contained in the graph.
+    method.update(
+            { 
+               ComputeGranularity:    args.compute_granularity,
+               SpaceDiscretization:   args.fd_order,
+               TimeIntegrator:        args.time_integrator,
+               Remesh:                args.remesh_kernel,
+               Interpolation:         args.interpolation
+            }
+        )
+
+    problem = Problem(method=method)
+    problem.insert(poisson, 
+                   min_max_U, min_max_W, adapt_dt,
+                   splitting, 
+                   integrate_S,
+                   dump_fields,
+                   compute_mean_fields)
+    problem.build(args)
+    
+    # If a visu_rank was provided, and show_graph was set,
+    # display the graph on the given process rank.
+    if args.display_graph:
+        problem.display()
+    
+    # Create a simulation
+    # (do not forget to specify the t and dt parameters here)
+    simu = Simulation(start=args.tstart, end=args.tend, 
+                      nb_iter=args.nb_iter,
+                      max_iter=args.max_iter,
+                      dt0=args.dt, times_of_interest=args.dump_times,
+                      t=t, dt=dt)
+    simu.write_parameters(t, dt_cfl, dt_advec, dt, 
+            min_max_U.Finf, min_max_W.Finf, adapt_dt.equivalent_CFL,
+            filename='parameters.txt', precision=8)
+    
+    # Initialize vorticity, velocity, S on all topologies
+    problem.initialize_field(field=velo,  formula=init_velocity)
+    problem.initialize_field(field=vorti, formula=init_vorticity)
+    problem.initialize_field(field=phi,   formula=init_phi, nblobs=nblobs, rblob=rblob, 
+            without_ghosts=BLOB_INIT)
+    
+    # Finally solve the problem 
+    problem.solve(simu, dry_run=args.dry_run)
+    
+    # Finalize
+    problem.finalize()
+
+
+if __name__=='__main__':
+    from examples.example_utils import HysopArgParser, colors
+
+    class ParticleAboveSaltArgParser(HysopArgParser):
+        def __init__(self):
+            prog_name = 'sediment_deposit_levelset'
+            default_dump_dir = '{}/hysop_examples/{}'.format(HysopArgParser.tmp_dir(), 
+                    prog_name)
+
+            description=colors.color('HySoP Sediment Deposit Levelset Example: ', fg='blue',
+                    style='bold')
+            description+='\n'
+            description+='\nThis example focuses on a validation study for the '
+            description+='hybrid particle-mesh vortex method for sediment deposit '
+            description+='using the levelset method.'
+    
+            super(ParticleAboveSaltArgParser, self).__init__(
+                 prog_name=prog_name,
+                 description=description,
+                 default_dump_dir=default_dump_dir)
+
+        def _setup_parameters(self, args):
+            dim = args.ndim
+            if (dim not in (2,3)):
+                msg='Domain should be 2D or 3D.'
+                self.error(msg)
+
+
+    parser = ParticleAboveSaltArgParser()
+
+    parser.set_defaults(impl='cl', ndim=2,
+                        npts=(TANK_RATIO*DISCRETIZATION+1,DISCRETIZATION+1),
+                        box_origin=(0.0,), box_length=(1.0,), 
+                        tstart=0.0, tend=100.1,
+                        dt=1e-6, cfl=0.50, lcfl=0.50,
+                        dump_times=tuple(float(1*x) for x in range(100)),
+                        dump_freq=0)
+
+    parser.run(compute)
+
diff --git a/examples/shear_layer/shear_layer.py b/examples/shear_layer/shear_layer.py
index aae07000558b5a281eea1389cfd3a97a5d6ed3df..210494ba3d9296911d325324eebc629cc78d7c13 100644
--- a/examples/shear_layer/shear_layer.py
+++ b/examples/shear_layer/shear_layer.py
@@ -14,7 +14,7 @@ def compute(args):
     from hysop.constants import Implementation, AdvectionCriteria
 
     from hysop.operators import DirectionalAdvection, DirectionalDiffusion, \
-                                PoissonRotational, AdaptiveTimeStep,        \
+                                PoissonCurl, AdaptiveTimeStep,        \
                                 MinMaxFieldStatistics, StrangSplitting
 
     from hysop.methods import SpaceDiscretization, Remesh, TimeIntegrator, \
@@ -77,7 +77,7 @@ def compute(args):
     # Define parameters and field (time, timestep, viscosity, velocity, vorticity)
     t, dt = TimeParameters(dtype=args.dtype)
     velo  = VelocityField(domain=box, dtype=args.dtype)
-    vorti = VorticityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
     nu    = ViscosityParameter(initial_value=args.nu, const=True, dtype=args.dtype)
     
     ### Build the directional operators
@@ -89,27 +89,22 @@ def compute(args):
             velocity_cfl = args.cfl,
             variables = {velo: npts, vorti: npts},
             dt=dt, **extra_op_kwds)
-    #> Directional diffusion
-    diffusion = DirectionalDiffusion(implementation=impl,
-            name='stretching_diffusion',
-            fields=vorti, coeffs=nu, 
-            variables={vorti: npts}, 
-            dt=dt, **extra_op_kwds)
     #> Directional splitting operator subgraph
     splitting = StrangSplitting(splitting_dim=dim, order=args.strang_order)
-    splitting.push_operators(advec, diffusion)
+    splitting.push_operators(advec)
 
 
     ### Build standard operators
     #> Poisson operator to recover the velocity from the vorticity
-    poisson = PoissonRotational(name='poisson_rotational', 
+    poisson = PoissonCurl(name='poisson_curl', 
                             velocity=velo, vorticity=vorti, 
                             variables={velo:npts, vorti: npts}, 
                             projection=args.reprojection_frequency,
+                            diffusion=nu, dt=dt,
                             implementation=impl, **extra_op_kwds)
     #> We ask to dump the inputs and the outputs of this operator
-    poisson.dump_outputs(fields=(vorti,), frequency=args.dump_freq)
-    poisson.dump_outputs(fields=(velo,),  frequency=args.dump_freq)
+    poisson.dump_outputs(fields=(vorti,), frequency=args.dump_freq, **extra_op_kwds)
+    poisson.dump_outputs(fields=(velo,),  frequency=args.dump_freq, **extra_op_kwds)
     #> Operator to compute the infinite norm of the velocity
     min_max_U = MinMaxFieldStatistics(name='min_max_U', field=velo,
             Finf=True, implementation=impl, variables={velo:npts},
@@ -140,7 +135,7 @@ def compute(args):
     )
     problem = Problem(method=method)
     problem.insert(poisson, splitting, min_max_U, min_max_W, adapt_dt)
-    problem.build()
+    problem.build(args)
     
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
@@ -277,7 +272,7 @@ if __name__=='__main__':
 
     parser = ShearLayerArgParser()
 
-    parser.set_defaults(impl='cl', ndim=2, npts=(257,),
+    parser.set_defaults(impl='cl', ndim=2, npts=(256,),
                         box_origin=(0.0,), box_length=(1.0,), 
                         tstart=0.0, tend=1.25, 
                         dt=1e-4, cfl=0.5, lcfl=0.125,
diff --git a/examples/taylor_green/taylor_green.py b/examples/taylor_green/taylor_green.py
index 5d773183591056dccb3a4e85fc7a5ba4bf7b1efd..24cb866aebb5d9e3bb1c46a72b4e8ee517cd55c5 100644
--- a/examples/taylor_green/taylor_green.py
+++ b/examples/taylor_green/taylor_green.py
@@ -35,13 +35,14 @@ def init_vorticity(data, coords, component=None):
 def compute(args):
     from hysop import Box, Simulation, Problem, MPIParams
     from hysop.defaults import VelocityField, VorticityField, \
-                               EnstrophyParameter, TimeParameters
+                               EnstrophyParameter, TimeParameters, \
+                               ViscosityParameter
     from hysop.constants import Implementation, AdvectionCriteria, StretchingCriteria
 
     from hysop.operators import DirectionalAdvection, DirectionalStretchingDiffusion, \
                                 DirectionalDiffusion, DirectionalStretching,          \
                                 StaticDirectionalStretching, Diffusion,               \
-                                PoissonRotational, AdaptiveTimeStep,                  \
+                                PoissonCurl, AdaptiveTimeStep,                  \
                                 Enstrophy, MinMaxFieldStatistics, StrangSplitting,    \
                                 ParameterPlotter, Advection, MinMaxGradientStatistics
 
@@ -84,8 +85,9 @@ def compute(args):
     # Define parameters and field (time, timestep, velocity, vorticity, enstrophy)
     t, dt = TimeParameters(dtype=args.dtype)
     velo  = VelocityField(domain=box, dtype=args.dtype)
-    vorti = VorticityField(domain=box, dtype=args.dtype)
+    vorti = VorticityField(velocity=velo)
     enstrophy = EnstrophyParameter(dtype=args.dtype)
+    viscosity = ViscosityParameter(dtype=args.dtype, initial_value=(1.0/args.Re), const=True)
     
     ### Build the directional operators
     if (impl is Implementation.FORTRAN):
@@ -124,34 +126,17 @@ def compute(args):
                  vorticity = vorti,
                  variables = {velo: npts, vorti: npts},
                  dt=dt, **extra_op_kwds)
-    #> Directional diffusion
-    if (impl is Implementation.OPENCL):
-        diffuse = None
-        diffuse_dir = DirectionalDiffusion(implementation=impl,
-                 name='diffuse',
-                 fields = vorti,
-                 coeffs = (1.0/args.Re),
-                 variables = {vorti: npts},
-                 dt=dt, **extra_op_kwds)
-    else:
-        diffuse = Diffusion(
-                implementation=impl,
-                name='diffuse',
-                Fin = vorti,
-                viscosity = (1.0/args.Re),
-                variables = {vorti: npts},
-                dt=dt, **extra_op_kwds)
-        diffuse_dir = None
     
     ### Build standard operators
     #> Poisson operator to recover the velocity from the vorticity
-    poisson = PoissonRotational(name='poisson', velocity=velo, vorticity=vorti, 
+    poisson = PoissonCurl(name='poisson', velocity=velo, vorticity=vorti, 
                             variables={velo:npts, vorti: npts}, 
                             projection=args.reprojection_frequency,
+                            diffusion=viscosity, dt=dt,
                             implementation=impl, **extra_op_kwds)
     #> We ask to dump the outputs of this operator
-    #poisson.dump_outputs(fields=(vorti,), frequency=args.dump_freq)
-    #poisson.dump_outputs(fields=(velo,),  frequency=args.dump_freq)
+    poisson.dump_outputs(fields=(vorti,), frequency=args.dump_freq, **extra_op_kwds)
+    poisson.dump_outputs(fields=(velo,),  frequency=args.dump_freq, **extra_op_kwds)
     
     #> Operator to compute the infinite norm of the velocity
     if (impl is Implementation.FORTRAN):
@@ -160,7 +145,8 @@ def compute(args):
             Finf=True, implementation=impl, variables={velo:npts},
             **extra_op_kwds)
     min_max_gradU = MinMaxGradientStatistics(F=velo,
-            Finf=True, implementation=impl, variables={velo:npts})
+            Finf=True, implementation=impl, variables={velo:npts},
+            **extra_op_kwds)
     #> Operator to compute the infinite norm of the vorticity
     min_max_W = MinMaxFieldStatistics(name='min_max_W', field=vorti,
             Finf=True, implementation=impl, variables={vorti:npts},
@@ -171,12 +157,14 @@ def compute(args):
     
     #> Directional splitting operator subgraph
     splitting = StrangSplitting(splitting_dim=dim, order=args.strang_order)
-    splitting.push_operators(advec_dir, stretch_dir, diffuse_dir, min_max_gradU)
+    splitting.push_operators(advec_dir, stretch_dir, min_max_gradU)
 
     ### Adaptive timestep operator
     adapt_dt = AdaptiveTimeStep(dt, equivalent_CFL=True)
-    dt_cfl   = adapt_dt.push_cfl_criteria(cfl=args.cfl, Finf=min_max_U.Finf,
-                                            equivalent_CFL=True)
+    dt_cfl   = adapt_dt.push_cfl_criteria(cfl=args.cfl, 
+                                          Fmin=min_max_U.Fmin,
+                                          Fmax=min_max_U.Fmax,
+                                          equivalent_CFL=True)
     dt_stretch = adapt_dt.push_stretching_criteria(gradFinf=min_max_gradU.Finf, 
             criteria=StretchingCriteria.GRAD_U)
     dt_lcfl0 = adapt_dt.push_advection_criteria(lcfl=args.lcfl, Finf=min_max_W.Finf, 
@@ -193,9 +181,9 @@ def compute(args):
             def __init__(self, **kwds):
                 import matplotlib.pyplot as plt
                 if all(n==npts[0] for n in npts):
-                    snpts='${}^3$'.format(npts[0]-1)
+                    snpts='${}^3$'.format(npts[0])
                 else:
-                    snpts='x'.join(str(n-1) for n in npts)
+                    snpts='x'.join(str(n) for n in npts)
                 tag='hysop-{}'.format(snpts)
                 fig  = plt.figure(figsize=(30,18))
                 axe0 = plt.subplot2grid((3,2), (0,0), rowspan=3, colspan=1)
@@ -267,11 +255,10 @@ def compute(args):
             }
     )
     problem = Problem(method=method)
-    problem.insert(poisson, 
-                   advec, splitting, diffuse,
+    problem.insert(poisson, advec, splitting,
                    min_max_U, min_max_W, enstrophy_op, 
                    adapt_dt, plot)
-    problem.build()
+    problem.build(args)
 
     # If a visu_rank was provided, and show_graph was set,
     # display the graph on the given process rank.
@@ -372,12 +359,12 @@ if __name__=='__main__':
 
     parser = TaylorGreenArgParser()
 
-    parser.set_defaults(impl='cl', ndim=3, npts=(65,),
+    parser.set_defaults(impl='cl', ndim=3, npts=(64,),
                         box_origin=(0.0,), box_length=(2*pi,), 
                         tstart=0.0, tend=20.01, 
                         dt=1e-5, 
                         cfl=0.5, lcfl=0.125,
-                        dump_freq=100, dump_times=(),
+                        dump_freq=0, dump_times=(),
                         Re=1600.0)
 
     parser.run(compute)
diff --git a/hysop/__init__.py.in b/hysop/__init__.py.in
index 36e159a3a4b4f7501c41da449220f508e33e35cf..9a74933c94835d27dbb5698ee404841ddb70f52b 100644
--- a/hysop/__init__.py.in
+++ b/hysop/__init__.py.in
@@ -1,7 +1,8 @@
-"""Python package dedicated to flow simulation using particular methods
+"""
+Python package dedicated to flow simulation using particular methods
 on hybrid architectures (MPI-GPU)
-
 """
+import psutil
 from functools import wraps
 from hysop.deps import __builtin__, print_function, os, sys, warnings, traceback
 
@@ -18,14 +19,18 @@ def get_env(target, default_value):
     else:
         return default_value
 
+def set_env(target, value):
+    if target not in os.environ:
+        os.environ[target] = str(value)
+
 # HySoP
 package_name = "@PACKAGE_NAME@"
 version      = "@HYSOP_VERSION@"
 
 # Compilation flags
-__MPI_ENABLED__    = "@USE_MPI@"     is "ON"
-__GPU_ENABLED__    = "@WITH_GPU@"    is "ON"
-__FFTW_ENABLED__   = "@WITH_FFTW@"   is "ON"
+__MPI_ENABLED__    = "@USE_MPI@" is "ON"
+__GPU_ENABLED__    = "@WITH_GPU@" is "ON"
+__FFTW_ENABLED__   = "@WITH_FFTW@" is "ON"
 __SCALES_ENABLED__ = "@WITH_SCALES@" is "ON"
 __OPTIMIZE__       = not __debug__
 
@@ -33,16 +38,30 @@ __VERBOSE__        = get_env('VERBOSE', ("@VERBOSE@" is "ON"))
 __DEBUG__          = get_env('DEBUG',   ("@DEBUG@" is "ON"))
 __PROFILE__        = get_env('PROFILE', ("@PROFILE@" is "ON"))
 
-__TRACE_CALLS__   = get_env('TRACE_CALLS',   False)
-__TRACE_WARNINGS__ = get_env('TRACE_WARNINGS', False)
+__TRACE_CALLS__     = get_env('TRACE_CALLS',   False)
+__TRACE_WARNINGS__  = get_env('TRACE_WARNINGS', False)
 __TRACE_MEMALLOCS__ = get_env('TRACE_MEMALLOC', False)
-__TRACE_KERNELS__  = get_env('TRACE_KERNELS', False)
-__KERNEL_DEBUG__   = get_env('KERNEL_DEBUG', False)
+__TRACE_KERNELS__   = get_env('TRACE_KERNELS', False)
+__KERNEL_DEBUG__    = get_env('KERNEL_DEBUG', False)
 __BACKTRACE_BIG_MEMALLOCS__ = get_env('BACKTRACE_BIG_MEMALLOCS', False)
 
 __TEST_ALL_OPENCL_PLATFORMS__ = get_env('TEST_ALL_OPENCL_PLATFORMS', False)
 __ENABLE_LONG_TESTS__ = get_env('ENABLE_LONG_TESTS', ("@ENABLE_LONG_TESTS@" is "ON"))
 
+# Threads
+__ENABLE_THREADING__ = get_env('ENABLE_THREADING', True)
+__MAX_THREADS__ = int(get_env('MAX_THREADS', psutil.cpu_count(logical=False)) if __ENABLE_THREADING__ else 1)
+set_env('OMP_NUM_THREADS',   __MAX_THREADS__)
+set_env('MKL_NUM_THREADS',   __MAX_THREADS__)
+set_env('NUMBA_NUM_THREADS', __MAX_THREADS__)
+set_env('NUMBA_THREADING_LAYER', 'workqueue') # Use 'numba -s' to list support
+__DEFAULT_NUMBA_TARGET__ = ('parallel' if __ENABLE_THREADING__ else 'cpu')
+
+# FFTW
+__FFTW_NUM_THREADS__       = int(get_env('FFTW_NUM_THREADS', __MAX_THREADS__))
+__FFTW_PLANNER_EFFORT__    = get_env('FFTW_PLANNER_EFFORT', 'FFTW_ESTIMATE')
+__FFTW_PLANNER_TIMELIMIT__ = int(get_env('FFTW_PLANNER_TIMELIMIT', -1))
+
 # OpenCL
 __DEFAULT_PLATFORM_ID__ = int(get_env('DEFAULT_PLATFORM_ID', @OPENCL_DEFAULT_OPENCL_PLATFORM_ID@))
 __DEFAULT_DEVICE_ID__   = int(get_env('DEFAULT_DEVICE_ID', @OPENCL_DEFAULT_OPENCL_DEVICE_ID@))
@@ -108,13 +127,13 @@ from hysop.parameters.scalar_parameter import ScalarParameter
 from hysop.parameters.tensor_parameter import TensorParameter
 from hysop.topology.cartesian_topology import Topology, CartesianTopology
 from hysop.topology.topology_descriptor  import TopologyDescriptor
-from hysop.tools.parameters   import Discretization, MPIParams
+from hysop.tools.parameters   import CartesianDiscretization, MPIParams
 from hysop.simulation         import Simulation
 from hysop.problem            import Problem
 from hysop.tools.io_utils     import IO, IOParams
 __all__ = ['Box', 'Field', 'DiscreteField', 'ScalarParameter', 'TensorParameter',
-           'Domain', 'Discretization', 'Simulation', 'IterativeMethod', 'MPIParams',
-           'Problem', 'IO', 'IOParams',
+           'Domain', 'CartesianDiscretization', 'Simulation', 'MPIParams',
+           'Problem', 'IO', 'IOParams', 'IterativeMethod',
            'Topology', 'CartesianTopology', 'TopologyDescriptor']
 if __MPI_ENABLED__:
     __all__ += ['MPI', 'main_rank', 'main_size']
@@ -137,4 +156,34 @@ cache_path   = IO.default_cache_path()
 msg_io =  '\n*Default path for all i/o is \'{}\'.'.format(default_path)
 msg_io += '\n*Default path for caching is \'{}\'.'.format(cache_path)
 mprint(msg_io)
-mprint()
+
+msg_threads = \
+'''
+*Threading configuration is:
+  --------------------------------
+  HYSOP_ENABLE_THREADING: {}
+  HYSOP_MAX_THREADS:      {}
+  --------------------------------
+  OMP_NUM_THREADS:        {}
+  MKL_NUM_THREADS:        {}
+  --------------------------------
+  DEFAULT_NUMBA_TARGET:   {}
+  NUMBA_THREADING_LAYER:  {}
+  NUMBA_NUM_THREADS:      {}
+  --------------------------------
+  FFTW_NUM_THREADS:       {}
+  FFTW_PLANNER_EFFORT:    {}
+  FFTW_PLANNER_TIMELIMIT: {}
+  --------------------------------
+'''.format(
+    __ENABLE_THREADING__,
+    __MAX_THREADS__,
+    os.environ['OMP_NUM_THREADS'],
+    os.environ['MKL_NUM_THREADS'],
+    __DEFAULT_NUMBA_TARGET__,
+    os.environ['NUMBA_THREADING_LAYER'],
+    os.environ['NUMBA_NUM_THREADS'],
+    __FFTW_NUM_THREADS__,
+    __FFTW_PLANNER_EFFORT__,
+    __FFTW_PLANNER_TIMELIMIT__)
+mprint(msg_threads)
diff --git a/hysop/backend/device/autotunable_kernel.py b/hysop/backend/device/autotunable_kernel.py
index a583569cada8681aaf737d9755c564b0093609fb..32d2d6359606ce48f4becb1d79becd79d39e5c35 100644
--- a/hysop/backend/device/autotunable_kernel.py
+++ b/hysop/backend/device/autotunable_kernel.py
@@ -25,123 +25,146 @@ class AutotunableKernel(object):
     def custom_hash(self, *args, **kwds):
         HASH_DEBUG=False
         assert args or kwds, 'no arguments to be hashed.'
+
         def _hash_arg(a):
+            s = ''
             if (a is None):
-                return hash('None')
+                s += '\nNone'
+                h = hash('None')
             elif (a is Ellipsis):
-                return hash('Ellipsis')
+                s += '\nEllipsis'
+                h = hash('Ellipsis')
             elif isinstance(a, str):
                 if HASH_DEBUG:
-                    print '>HASHING STR: {}'.format(a)
+                    s += '\n>HASHING STR: {}'.format(a)
                 h = hash(a)
                 if HASH_DEBUG:
-                    print '<HASHED STR: hash={}'.format(h)
+                    s += '\n<HASHED STR: hash={}'.format(h)
             elif isinstance(a, list):
                 if HASH_DEBUG:
-                    print '>HASHING LIST:'
+                    s += '\n>HASHING LIST:'
                 h = hash(tuple(_hash_arg(x) for x in a))
                 if HASH_DEBUG:
-                    print '<HASHED LIST: hash={}'.format(h)
+                    s += '\n<HASHED LIST: hash={}'.format(h)
             elif isinstance(a, tuple):
                 if HASH_DEBUG:
-                    print '>HASHING TUPLE:'
+                    s += '\n>HASHING TUPLE:'
                 h = hash(tuple(_hash_arg(x) for x in a))
                 if HASH_DEBUG:
-                    print '<HASHED TUPLE: hash={}'.format(h)
+                    s += '\n<HASHED TUPLE: hash={}'.format(h)
             elif isinstance(a, (set,frozenset)):
                 if HASH_DEBUG:
-                    print '>HASHING SET:'
+                    s += '\n>HASHING SET:'
                 h = hash(tuple(_hash_arg(x) for x in sorted(a)))
                 if HASH_DEBUG:
-                    print '<HASHED SET: hash={}'.format(h)
+                    s += '\n<HASHED SET: hash={}'.format(h)
             elif isinstance(a, dict):
                 if HASH_DEBUG:
-                    print '>HASHING DICT:'
+                    s += '\n>HASHING DICT:'
                 h =  hash(tuple((_hash_arg(k), _hash_arg(a[k])) for k in sorted(a.keys())))
                 if HASH_DEBUG:
-                    print '<HASHED DICT: hash={}'.format(h)
+                    s += '\n<HASHED DICT: hash={}'.format(h)
             elif isinstance(a, npw.ndarray):
                 if HASH_DEBUG:
-                    print '>HASHING NDARRAY:'
+                    s += '\n>HASHING NDARRAY:'
                 assert a.ndim <= 1
                 assert a.size < 17, 'Only parameters up to size 16 are allowed.'
-                h =  self.custom_hash(a.tolist())
+                hh, ss =  self.custom_hash(a.tolist())
+                h = hh
+                s += ss
                 if HASH_DEBUG:
-                    print '>HASHED NDARRAY: hash={}'.format(h)
+                    s += '\n>HASHED NDARRAY: hash={}'.format(h)
             else:
                 h = hash(a)
                 if HASH_DEBUG:
-                    print '>HASHED UNKNOWN TYPE {}: hash={}'.format(type(a), h)
+                    s += '\n>HASHED UNKNOWN TYPE {}: hash={}'.format(type(a), h)
             assert (h is not id(a)), type(a)
-            return h
+            return h, s
+
         def _hash_karg(k,v):
+            s = ''
             if (k == 'mesh_info_vars'):
                 # for mesh infos we just hash the code generated constants that 
                 # may alter the code branching.
                 if HASH_DEBUG:
-                    print '<HASHING MESHINFO'
+                    s += '\n<HASHING MESHINFO'
                 from hysop.backend.device.codegen.base.variables import CodegenStruct
                 check_instance(v, dict, keys=str, values=CodegenStruct)
                 mesh_infos = tuple(str(v[k]) for k in sorted(v.keys()))
                 h = hash(mesh_infos)
                 if HASH_DEBUG:
-                    print '  MESH INFOS:'
+                    s += '\n  MESH INFOS:'
                     for mi in mesh_infos:
-                        print '     '+mi
-                    print '>HASHED MESHINFO: hash={}'.format(h)
-                return h
+                        s += '\n     '+mi
+                    s += '\n>HASHED MESHINFO: hash={}'.format(h)
+                return h, s
             elif (k == 'expr_info'):
                 # for expr infos we just hash the continous and discrete expressions  
                 # and some additional variables
                 if HASH_DEBUG:
-                    print '>HASHING EXPR_INFO:'
+                    s += '\n>HASHING EXPR_INFO:'
                 exprs =  tuple(str(e) for e in v.exprs)
                 exprs += tuple(str(e) for e in v.dexprs)
                 extras = (v.name, v.direction, v.has_direction, v.dt_coeff, v.kind)
                 for k in sorted(v.min_ghosts_per_components.keys(), key=lambda x: x.name):
                     extras += (k.name, _hash_arg(v.min_ghosts_per_components[k]))
-                for k in sorted(v.input_params, key=lambda x: x[0]):
-                    extras += (k, hash(v.input_params[k].short_description()))
-                for k in sorted(v.output_params, key=lambda x: x[0]):
-                    extras += (k, hash(v.output_params[k].short_description()))
-                h = self.custom_hash(exprs + extras)
+                for mem_obj_key in ('input_arrays',   'output_arrays', 
+                                    'input_buffers',  'output_buffers',
+                                    'input_params',   'output_params'):
+                    mem_objects = getattr(v, mem_obj_key)
+                    for k in sorted(mem_objects, key=lambda x: x[0]):
+                        assert hasattr(mem_objects[k], 'short_description'), type(mem_objects[k]).__mro__
+                        extras += (k, hash(mem_objects[k].short_description()))
+                hh, ss = self.custom_hash(exprs + extras)
+                h = hh
+                s += ss
                 if HASH_DEBUG:
-                    print '  EXPRESSIONS:'
+                    s += '\n  EXPRESSIONS:'
                     for e in exprs:
-                        print '    ', e, type(e)
-                        print '       with hash {}'.format(self.custom_hash(e))
-                    print '  EXTRAS:'
+                        s += '\n    {} {}'.format(e, type(e))
+                        s += '\n       with hash {}'.format(self.custom_hash(e)[1])
+                    s += '\n  EXTRAS:'
                     for e in extras:
-                        print '    ', e, type(e)
-                        print '       with hash {}'.format(self.custom_hash(e))
-                    print '<HASHED EXPR_INFO: hash={}'.format(h)
-                return h
+                        s += '\n    {} {}'.format(e, type(e))
+                        s += '\n       with hash {}'.format(self.custom_hash(e)[1])
+                    s += '\n<HASHED EXPR_INFO: hash={}'.format(h)
+                return h, s
             else:
                 msg='Unknown custom hash key \'{}\'.'.format(k)
                 raise KeyError(msg)
-
-        h = None
-        if args:
-            h = _hash_arg(args[0])
-            if HASH_DEBUG:
-                print 'HASHED ARGUMENT 0: {}'.format(h)
-            for (i,arg) in enumerate(args[1:]):
-                h ^= _hash_arg(arg)
-                if HASH_DEBUG:
-                    print 'HASHED ARGUMENT {}: {}'.format(i, h)
-        if kwds: 
-            items = sorted(kwds.items(), key=lambda x: x[0])
-            if (h is None):
-                h  = _hash_karg(*items[0])
-            else:
-                h ^= _hash_karg(*items[0])
-            if HASH_DEBUG:
-                print 'HASHED KWD 0: {}'.format(h)
-            for (i,it) in enumerate(items[1:]):
-                h ^= _hash_karg(*it)
+        
+        def hash_all(*args, **kwds):
+            h, s = None, None
+            if args:
+                h, s = _hash_arg(args[0])
                 if HASH_DEBUG:
-                    print 'HASHED KWD {}: {}'.format(i, h)
-        return h
+                    s += '\nHASHED ARGUMENT 0: {}'.format(h)
+                for (i,arg) in enumerate(args[1:]):
+                    hh, ss = _hash_arg(arg)
+                    h ^= hh
+                    if HASH_DEBUG:
+                        s += ss
+                        s += '\nHASHED ARGUMENT {}: {}'.format(i, h)
+            if kwds: 
+                items = sorted(kwds.items(), key=lambda x: x[0])
+                if (h is None):
+                    h, s  = _hash_karg(*items[0])
+                else:
+                    hh, ss = _hash_karg(*items[0])
+                    h ^= hh
+                    if HASH_DEBUG:
+                        s += ss
+                        s += '\nHASHED KWD 0: {}'.format(h)
+                for (i,it) in enumerate(items[1:]):
+                    hh, ss = _hash_karg(*it)
+                    h ^= hh
+                    if HASH_DEBUG:
+                        s += ss
+                        s += '\nHASHED KWD {}: {}'.format(i, h)
+            return h, s
+
+        h, s = hash_all(*args, **kwds)
+        return h, s
         
     
     @abstractmethod
@@ -183,7 +206,8 @@ class AutotunableKernel(object):
     
     @abstractmethod
     def format_best_candidate(self, extra_kwds, extra_parameters, work_load,
-            global_work_size, local_work_size, kernel, kernel_statistics, src_hash):
+            global_work_size, local_work_size, kernel, kernel_statistics, 
+            src_hash, hash_logs):
         """
         Post treatment callback for autotuner results.
         Transform autotuner results in user friendly kernel wrappers.
@@ -194,15 +218,22 @@ class AutotunableKernel(object):
         """Register extra parameters to optimize."""
         return AutotunerParameterConfiguration()
     
-    def compute_work_bounds(self, extra_parameters, extra_kwds,
+    def compute_work_bounds(self, 
+            max_kernel_work_group_size,
+            preferred_work_group_size_multiple,
+            extra_parameters, extra_kwds,
             work_size=None, work_dim=None, 
             min_work_load=None, max_work_load=None):
         """
         Configure work_bounds (work_dim, work_size, max_work_load).
         Return a WorkBoundsConfiguration object.
         """
+        check_instance(max_kernel_work_group_size, (int,long))
+        check_instance(preferred_work_group_size_multiple, (int, long))
         check_instance(extra_parameters, dict, keys=str)
         check_instance(extra_kwds, dict, keys=str)
+        assert (max_kernel_work_group_size>0), max_kernel_work_group_size
+        assert (preferred_work_group_size_multiple>0), preferred_work_group_size_multiple
 
         msg='FATAL ERROR: Could not extract {} from keyword arguments, '
         msg+= 'extra_parameters and extra_kwds.'
@@ -243,11 +274,15 @@ class AutotunableKernel(object):
         max_device_work_group_size = self.max_device_work_group_size()
         max_device_work_item_sizes = self.max_device_work_item_sizes()
 
-        work_bounds = AutotunerWorkBoundsConfiguration(work_dim=work_dim, work_size=work_size, 
+        max_work_group_size = min(max_device_work_group_size, max_kernel_work_group_size)
+
+        work_bounds = AutotunerWorkBoundsConfiguration(
+                work_dim=work_dim, work_size=work_size, 
                 min_work_load=min_work_load, max_work_load=max_work_load,
                 max_device_work_dim=max_device_work_dim,
-                max_device_work_group_size=max_device_work_group_size, 
-                max_device_work_item_sizes=max_device_work_item_sizes)
+                max_device_work_group_size=max_work_group_size, 
+                max_device_work_item_sizes=max_device_work_item_sizes,
+                preferred_work_group_size_multiple=preferred_work_group_size_multiple)
         return work_bounds
 
     def compute_work_candidates(self, work_bounds, work_load,
@@ -487,6 +522,7 @@ class AutotunerWorkBoundsConfiguration(object):
     def __init__(self, work_dim, work_size, 
             min_work_load, max_work_load, 
             max_device_work_dim, max_device_work_group_size, max_device_work_item_sizes,
+            preferred_work_group_size_multiple,
             **kwds):
 
         super(AutotunerWorkBoundsConfiguration, self).__init__(**kwds)
@@ -495,6 +531,7 @@ class AutotunerWorkBoundsConfiguration(object):
 
         work_dim = int(work_dim)
         assert (work_dim > 0)
+        assert(preferred_work_group_size_multiple>0), preferred_work_group_size_multiple
 
         work_size = npw.asarray(work_size, dtype=npw.int32)
         min_work_load = npw.asarray(min_work_load, dtype=npw.int32)
@@ -516,6 +553,8 @@ class AutotunerWorkBoundsConfiguration(object):
         self._max_device_work_group_size = int(max_device_work_group_size)
         self._max_device_work_item_sizes = npw.asarray(max_device_work_item_sizes[:work_dim], dtype=npw.int32)
 
+        self._preferred_work_group_size_multiple = preferred_work_group_size_multiple
+
         self._generate_work_loads()
 
     def _get_work_dim(self):
@@ -532,6 +571,8 @@ class AutotunerWorkBoundsConfiguration(object):
         return self._max_device_work_group_size
     def _get_max_device_work_item_sizes(self):
         return self._max_device_work_item_sizes
+    def _get_preferred_work_group_size_multiple(self):
+        return self._preferred_work_group_size_multiple
 
     work_dim = property(_get_work_dim)
     work_size = property(_get_work_size)
@@ -540,6 +581,7 @@ class AutotunerWorkBoundsConfiguration(object):
     max_device_work_dim = property(_get_max_device_work_dim)
     max_device_work_group_size = property(_get_max_device_work_group_size)
     max_device_work_item_sizes = property(_get_max_device_work_item_sizes)
+    preferred_work_group_size_multiple = property(_get_preferred_work_group_size_multiple)
 
     def _generate_work_loads(self):
         work_size = self.work_size
diff --git a/hysop/backend/device/codegen/base/codegen.py b/hysop/backend/device/codegen/base/codegen.py
index b6cc0c8112aae9dda5064a5e614284a3d868d56a..4f6cfbd38c5bfc916a511c5b20c9576019548cf3 100644
--- a/hysop/backend/device/codegen/base/codegen.py
+++ b/hysop/backend/device/codegen/base/codegen.py
@@ -203,7 +203,7 @@ class CodeGenerator(object):
         self.append(code)
     def define(self,what,prepend=True):
         code = '#define {}'.format(what)
-        self.append(code,simple=True)
+        self.append(code)
     def include(self,*args):
         code = []
         for k in args:
diff --git a/hysop/backend/device/codegen/base/kernel_codegen.py b/hysop/backend/device/codegen/base/kernel_codegen.py
index 812129eff3097509902c2e33820fd38cdfa4da1a..1c44d051014aa03f982308db197cef3dd70a3cf5 100644
--- a/hysop/backend/device/codegen/base/kernel_codegen.py
+++ b/hysop/backend/device/codegen/base/kernel_codegen.py
@@ -24,14 +24,14 @@ class KernelCodeGenerator(KernelBase, OpenClCodeGenerator):
     
     def __init__(self,name,typegen,work_dim,symbolic_mode=True,
             kernel_args=None, known_vars=None,
-            vec_type_hint=None):
+            vec_type_hint=None, **kwds):
             
         kernel_args = ArgDict()       if (kernel_args is None) else kernel_args
         known_vars  = WriteOnceDict() if (known_vars is None)  else known_vars
 
         check_instance(typegen,OpenClTypeGen)
         check_instance(kernel_args,ArgDict)
-        assert work_dim>0 and work_dim<=3
+        assert work_dim>0 and work_dim<=3, work_dim
 
         if (vec_type_hint is not None):
             if (vec_type_hint not in typegen.builtin_types):
@@ -55,7 +55,7 @@ class KernelCodeGenerator(KernelBase, OpenClCodeGenerator):
                 typegen=typegen,
                 symbolic_mode=symbolic_mode,
                 kernel_args=kernel_args, known_args=known_args,
-                known_vars=known_vars)
+                known_vars=known_vars, **kwds)
         
         self.inject_vars(kernel_args)
         self.symbolic_mode=symbolic_mode
diff --git a/hysop/backend/device/codegen/base/opencl_codegen.py b/hysop/backend/device/codegen/base/opencl_codegen.py
index 99bc18e05916b7818d0e764bd85b25dd32314609..eb77f99831e94b4a7b9be37c06ce76ff58a7b034 100644
--- a/hysop/backend/device/codegen/base/opencl_codegen.py
+++ b/hysop/backend/device/codegen/base/opencl_codegen.py
@@ -45,7 +45,7 @@ class OpenClCodeGenerator(CodeGenerator):
         
         if declare_cl_exts:
             for cl_ext in typegen.cl_requirements():
-                if cl_ext is not None:
+                if (cl_ext is not None):
                     self.declare_cl_extension(cl_ext)
     
 
diff --git a/hysop/backend/device/codegen/base/variables.py b/hysop/backend/device/codegen/base/variables.py
index 3924583104befe1d7e844c9ec012d85006a77b7d..e10575633fd63f093f49e79e215e0b9302e6157d 100644
--- a/hysop/backend/device/codegen/base/variables.py
+++ b/hysop/backend/device/codegen/base/variables.py
@@ -738,12 +738,16 @@ class CodegenVectorClBuiltin(CodegenVector):
         if (dim > 1):
             ctype = btype+str(dim)
             access_mode = access_mode if access_mode else ('pos' if dim<=4 else 'hex')
+            msg='Wrong vector size {}'.format(dim)
+            assert dim in typegen.vsizes
+            msg='Invalid basetype {} for vector.'.format(btype)
+            assert btype in (typegen.float_base_types + typegen.signed_base_types + typegen.unsigned_base_types), msg
+            msg='Invalid builtin type {}.'.format(ctype)
+            assert ctype in typegen.builtin_types, ctype
         else:
+            # scalar type
             ctype = btype
             access_mode=None
-        assert dim in typegen.vsizes
-        assert btype in (typegen.float_base_types + typegen.signed_base_types + typegen.unsigned_base_types)
-        assert ctype in typegen.builtin_types
         
         svalue = None
         if (value is not None):
@@ -847,14 +851,15 @@ class CodegenVectorClBuiltin(CodegenVector):
                 value = [self.svalue[i] for i in key]
                 return '({})({})'.format(ctype, ','.join(value))
             return access
-        elif isinstance(key, int) :
+        elif isinstance(key, (int,long)) :
             if key<0:
                 key += dim
             if key<0 or key>=dim:
                 raise IndexError, "The index {} is out of range.".format(key)
             return self.sval(key)
         else:
-            raise TypeError, 'Invalid key type!'
+            msg='Invalid key type {}!'.format(type(key))
+            raise TypeError(msg)
     
     def declare(self, codegen=None, init=None, **kargs):
         init = init or self.init
diff --git a/hysop/backend/device/codegen/kernels/custom_symbolic.py b/hysop/backend/device/codegen/kernels/custom_symbolic.py
index 3fcd9ae28fed211fe596ae1a24383e16799aae6a..70cb03c8d9b95e360b84e9d81b8044ae23b9e3f6 100644
--- a/hysop/backend/device/codegen/kernels/custom_symbolic.py
+++ b/hysop/backend/device/codegen/kernels/custom_symbolic.py
@@ -242,8 +242,8 @@ class SymbolicCodegenContext(object):
         for dfield in dfields:
             field = dfield._field
             ctype = dfield.ctype
-            name = dfield.name.lower()
-            if (name == dfield.name):
+            name = dfield.var_name.lower()
+            if (name == dfield.var_name):
                 name = '_'+name
             name = '{}_{{}}'.format(name)
             reads  = read_counter.get(dfield, None)
@@ -421,7 +421,7 @@ class CustomSymbolicKernelGenerator(KernelCodeGenerator):
                        kernel_dim, work_dim, granularity,
                        vectorization, 
                        itype='int',
-                       use_short_circuit=None,
+                       use_short_circuit = None,
                        symbolic_mode = False,
                        debug_mode    = False, 
                        tuning_mode   = False,
@@ -533,7 +533,7 @@ class CustomSymbolicKernelGenerator(KernelCodeGenerator):
                 args    = array_args.setdefault(obj, {})
                 strides = array_strides.setdefault(obj, {})
 
-                mesh_info_name = '{}_mesh_info'.format(dfield.name)
+                mesh_info_name = '{}_mesh_info'.format(dfield.var_name)
                 mesh_info = kernel_reqs['MeshInfoStruct'].build_codegen_variable(
                         const=True, name=mesh_info_name)
                 assert dfield not in mesh_infos
@@ -544,7 +544,7 @@ class CustomSymbolicKernelGenerator(KernelCodeGenerator):
                         continue
                     if (dfield in di.write_counter) and di.write_counter[dfield][i]>0:
                         continue
-                    vname = dfield.name + '_' + str(i)
+                    vname = dfield.var_name + '_' + str(i)
                     (arg, stride) = OpenClArrayBackend.build_codegen_arguments(kargs, name=vname,
                                 known_vars=csc.known_vars, symbolic_mode=csc.symbolic_mode,
                                 storage=self._global, ctype=dfield.ctype, 
@@ -585,7 +585,7 @@ class CustomSymbolicKernelGenerator(KernelCodeGenerator):
                 args    = array_args.setdefault(dfield, {})
                 strides = array_strides.setdefault(dfield, {})
                 if (dfield not in mesh_infos):
-                    mesh_info_name = '{}_mesh_info'.format(dfield.name)
+                    mesh_info_name = '{}_mesh_info'.format(dfield.var_name)
                     mesh_info = kernel_reqs['MeshInfoStruct'].build_codegen_variable(
                             const=True, name=mesh_info_name)
                     mesh_infos[dfield] = mesh_info_name
@@ -593,7 +593,7 @@ class CustomSymbolicKernelGenerator(KernelCodeGenerator):
                 for (i, count) in enumerate(counts):
                     if (count==0):
                         continue
-                    vname = dfield.name + '_' + str(i)
+                    vname = dfield.var_name + '_' + str(i)
                     arg, arg_strides = OpenClArrayBackend.build_codegen_arguments(kargs, 
                                 name=vname,
                                 known_vars=csc.known_vars, symbolic_mode=csc.symbolic_mode,
@@ -750,6 +750,8 @@ class CustomSymbolicKernelGenerator(KernelCodeGenerator):
         for (array, array_data) in array_args.iteritems():
             if isinstance(array, OpenClSymbolicArray):
                 name = array.varname
+            elif isinstance(array, DiscreteScalarFieldView):
+                name = array.var_name
             else:
                 name = array.name
             vindex = CodegenVectorClBuiltin(name+'_vid', itype, varray_dim, typegen=tg)
diff --git a/hysop/backend/device/codegen/kernels/directional_advection.py b/hysop/backend/device/codegen/kernels/directional_advection.py
index 98f4cc35cd8b8b8c96c341921d973b263fe6a0be..0e7033c41bea25bc62a481c7a6b1c19d893efaa6 100644
--- a/hysop/backend/device/codegen/kernels/directional_advection.py
+++ b/hysop/backend/device/codegen/kernels/directional_advection.py
@@ -89,8 +89,8 @@ class DirectionalAdvectionKernelGenerator(KernelCodeGenerator):
         if tuning_mode:
             unroll_loops = False
 
-        assert vboundary[0] in [BoundaryCondition.PERIODIC, BoundaryCondition.NONE]
-        assert vboundary[1] in [BoundaryCondition.PERIODIC, BoundaryCondition.NONE]
+        #assert vboundary[0] in [BoundaryCondition.PERIODIC, BoundaryCondition.NONE]
+        #assert vboundary[1] in [BoundaryCondition.PERIODIC, BoundaryCondition.NONE]
         is_periodic = (vboundary[0]==BoundaryCondition.PERIODIC \
                    and vboundary[1]==BoundaryCondition.PERIODIC)
         assert (is_periodic and not is_cached) or min_ghosts>0
@@ -295,6 +295,7 @@ class DirectionalAdvectionKernelGenerator(KernelCodeGenerator):
         nparticles = s.nparticles
         min_ghosts = s.min_ghosts
         field_infos = s.field_infos
+        tuning_mode = s.tuning_mode
 
         symbolic_mode = s.symbolic_mode
         use_short_circuit = s.use_short_circuit
@@ -599,11 +600,20 @@ class DirectionalAdvectionKernelGenerator(KernelCodeGenerator):
                 s.jumpline()
 
                 if is_cached and not has_bilevel:
-                    code='event_t event = async_work_group_copy({dst}, {src}, {ne}, {event});'.format(
-                            dst=Vc, src=line_velocity, ne=V_cache_width, event=0)
-                    s.append(code)
-                    code = 'wait_group_events(1, &event);'
-                    s.append(code)
+                    if tuning_mode:
+                        loop = 'int {i}={Lx}; {i}<{N}; {i}+={gsize}'.format(
+                                i='idx', N=V_cache_width, 
+                                Lx=local_id[0], gsize=local_size[0])
+                        with s._for_(loop):
+                            code='{dst}[{i}] = 0.5;'.format(i='idx', dst=Vc)
+                            s.append(code)
+                        s.barrier(_local=True)
+                    else:
+                        code='event_t event = async_work_group_copy({dst}, {src}, {ne}, {event});'.format(
+                                dst=Vc, src=line_velocity, ne=V_cache_width, event=0)
+                        s.append(code)
+                        code = 'wait_group_events(1, &event);'
+                        s.append(code)
                     s.jumpline()
                 elif has_bilevel and not velocity_cache_full_length:
                     # We must load velocity cache line on the fly
@@ -715,8 +725,9 @@ if __name__ == '__main__':
         min_ghosts=10,
         symbolic_mode=True,
         relative_velocity=0.66,
-        #nparticles=4,                          # MONOLEVEL TEST
-        nparticles=1, is_bilevel=(256,64,32), # BILEVEL TEST
+        nparticles=4,                        # MONOLEVEL TEST
+        tuning_mode = True,
+        #nparticles=1, is_bilevel=(256,64,32), # BILEVEL TEST
         known_vars=dict(
             V_mesh_info=vmesh_info,
             P_mesh_info=pmesh_info,
diff --git a/hysop/backend/device/codegen/kernels/tests/test_transpose.py b/hysop/backend/device/codegen/kernels/tests/test_transpose.py
index f0e3bbc91a29175084ba1516a74f0697e4aa0723..f5281606e9460729c2baac4d056d1b41501b4c4c 100644
--- a/hysop/backend/device/codegen/kernels/tests/test_transpose.py
+++ b/hysop/backend/device/codegen/kernels/tests/test_transpose.py
@@ -6,7 +6,7 @@ from hysop.deps import np, it
 from hysop.tools.misc import upper_pow2_or_3, prod
 from hysop.tools.types import check_instance
 from hysop.tools.numerics import is_integer
-from hysop.backend.device.opencl import cl, clTools
+from hysop.backend.device.opencl import cl, cl_api, clTools
 from hysop.backend.device.codegen.base.test import _test_typegen
 from hysop.backend.device.codegen.base.variables import dtype_to_ctype
 from hysop.backend.device.codegen.kernels.transpose import TransposeKernelGenerator
@@ -365,7 +365,7 @@ class TestTranspose(object):
                         buffers = (('Tin',  Tin_cpu,  Tin,  in_view),
                                    ('Tout', Tout_cpu, Tout, out_view))
                     good, err_buffers = self._cmp_buffers(buffers,dak,dim)
-                except cl.cffi_cl.RuntimeError as error:
+                except cl_api.RuntimeError as error:
                     e = error
                     print 'ERROR: ',e
                     good = False
diff --git a/hysop/backend/device/codegen/kernels/transpose.py b/hysop/backend/device/codegen/kernels/transpose.py
index 162bf87fe92438396a612d59279bf94ccd2a47ff..4d4701de045b93cf0c6f7e8424394aae6606c5fc 100644
--- a/hysop/backend/device/codegen/kernels/transpose.py
+++ b/hysop/backend/device/codegen/kernels/transpose.py
@@ -237,6 +237,8 @@ class TransposeKernelGenerator(KernelCodeGenerator):
                 symbolic_mode=symbolic_mode,
                 **kargs)
 
+        dtype = ctype_to_dtype(ctype)
+
         if debug_mode:
             print 'Transpose codegen configuration:'
             print ' *dimension:         {}'.format(pdim)
@@ -252,9 +254,11 @@ class TransposeKernelGenerator(KernelCodeGenerator):
             print ' *is_workload_index: {}'.format(is_workload_index)
             print ' *work_dim:          {} (tile[{}] + device_workload[{}])'.format(work_dim, 
                     tdim, work_dim-tdim)
+            print ' *ctype:             {}'.format(ctype)
+            print ' *dtype:             {}'.format(dtype)
         
-        self.dtype             = ctype_to_dtype(ctype)
         self.ctype             = ctype
+        self.dtype             = dtype
         self.axes              = axes
         self.pdim              = pdim
         self.Pdim              = Pdim
@@ -571,6 +575,14 @@ class TransposeKernelGenerator(KernelCodeGenerator):
                 for i in xrange(tdim)][::-1])
             tile_out0 = tile() + ''.join(['[{}]'.format(lidx[axes.tolist().index(axes[i])]) 
                 for i in xrange(tdim)])
+                
+        
+        #include complex definitions if required
+        with s._codeblock_('pragma_extensions'):
+            if (ctype == 'cdouble_t'):
+                s.define('PYOPENCL_DEFINE_CDOUBLE')
+            if ctype in ('cfloat_t', 'cdouble_t'):
+                s.include('"pyopencl-complex.h"')
 
         with s._kernel_():
             with s._align_() as al:
diff --git a/hysop/backend/device/codegen/symbolic/expr.py b/hysop/backend/device/codegen/symbolic/expr.py
index 4b20d5e007aa880d73412319226c502c219a9c64..f02f7daeebc9659ab74c4f740e473b1c465b9dca 100644
--- a/hysop/backend/device/codegen/symbolic/expr.py
+++ b/hysop/backend/device/codegen/symbolic/expr.py
@@ -163,6 +163,11 @@ class IntegerConstant(NumericalConstant):
     pass
 class FloatingPointConstant(NumericalConstant):
     pass
+class ComplexFloatingPointConstant(NumericalConstant):
+    def _ccode(self, printer):
+        return '(({})({}, {}))'.format(self.ctype, 
+                printer.typegen.dump(self.value.real),
+                printer.typegen.dump(self.value.imag))
 
 class OpenClVariable(TypedExpr):
     def __new__(cls, ctype, var, *args):
diff --git a/hysop/backend/device/codegen/symbolic/functions/custom_symbolic_function.py b/hysop/backend/device/codegen/symbolic/functions/custom_symbolic_function.py
index e0fb4712fc56e7ba4cdc5d0caea10e12287fabf1..7f2ba33185b2485e7002c65a2cc50d584b7654b2 100644
--- a/hysop/backend/device/codegen/symbolic/functions/custom_symbolic_function.py
+++ b/hysop/backend/device/codegen/symbolic/functions/custom_symbolic_function.py
@@ -15,12 +15,13 @@ from hysop.symbolic.field import SymbolicDiscreteField
 from hysop.symbolic.misc import ApplyStencil
 from hysop.symbolic.array import OpenClSymbolicArray, OpenClSymbolicBuffer
 from hysop.symbolic.tmp import TmpScalar
+from hysop.symbolic.spectral import WaveNumberIndex
 
 from hysop.backend.device.codegen.symbolic.expr import VLoad, VStore, \
         VStoreIf, VLoadIf, OpenClVariable, OpenClPrinter,  TypedI, \
         OpenClAssignment, OpenClCast, OpenClIndexedVariable, \
         NumericalConstant, FloatingPointConstant, IntegerConstant, \
-        FunctionCall, Return
+        FunctionCall, Return, ComplexFloatingPointConstant
 
 from hysop.backend.device.codegen.symbolic.map import map_expression, OpenClCastUtils
 
@@ -35,7 +36,7 @@ class CustomSymbolicFunction(OpenClFunctionCodeGenerator):
 
     @classmethod
     def field_name(cls, field, index):
-        return cls.varname('{}_{}'.format(field.name, index))
+        return cls.varname('{}_{}'.format(field.var_name, index))
     @classmethod
     def array_name(cls, array):
         return cls.varname(array.varname)
@@ -179,6 +180,8 @@ class CustomSymbolicFunction(OpenClFunctionCodeGenerator):
             pexpr = IntegerConstant('int', expr)
         elif isinstance(expr, (float, sm.Rational, sm.Float)):
             pexpr = FloatingPointConstant(csc.typegen.fbtype, expr)
+        elif isinstance(expr, complex):
+            pexpr = ComplexFloatingPointConstant(csc.typegen.fbtype+'2', expr)
         elif isinstance(expr, npw.number):
             ctype = dtype_to_ctype(expr)
             pexpr = NumericalConstant(ctype, expr)
@@ -264,6 +267,9 @@ class CustomSymbolicFunction(OpenClFunctionCodeGenerator):
             self.check_and_set(reqs, stencil_fn.name, stencil_fn)
             for argname, argval in stencil_fn.args.iteritems():
                 self.check_and_set(args, argname, argval)
+        elif isinstance(expr, WaveNumberIndex):
+            expr = expr.real_index
+            pexpr = self.parse_expr(csc, name, expr, args, reqs)
         elif isinstance(expr, sm.Symbol):
             sname = expr.name
             if (sname=='dx'):
@@ -278,7 +284,7 @@ class CustomSymbolicFunction(OpenClFunctionCodeGenerator):
                 self.check_and_set(args, i.name, i)
                 pexpr = OpenClVariable(i.ctype, i)
             else:
-                msg='Unknown symbol {}'.format(expr)
+                msg='Unknown symbol {} of type {}'.format(expr, type(expr))
                 raise NotImplementedError(msg)
         elif isinstance(expr, (sm.UnevaluatedExpr, UnsplittedExpr)):
             pexpr = self.parse_expr(csc, name, expr.args[0], args, reqs)
diff --git a/hysop/backend/device/codegen/symbolic/map.py b/hysop/backend/device/codegen/symbolic/map.py
index 7fe48311e5ecc3c4ec60b70341ead0066976c253..e6ce67e46858361d51ff60ea17a293bfb4bf104a 100644
--- a/hysop/backend/device/codegen/symbolic/map.py
+++ b/hysop/backend/device/codegen/symbolic/map.py
@@ -8,7 +8,8 @@ from hysop.symbolic.relational import ArithmeticRelation, LogicalRelation, \
                                       LogicalAND, LogicalOR, LogicalXOR, \
                                       LogicalEQ, LogicalNE,  \
                                       LogicalLT, LogicalGT,  \
-                                      LogicalLE, LogicalGE
+                                      LogicalLE, LogicalGE,  \
+                                      Min, Max
 from hysop.backend.device.codegen.symbolic.relational import basetype
 
 def map_expression(csc, expr, args, reqs):
@@ -37,7 +38,8 @@ def map_expression(csc, expr, args, reqs):
 
 
 def _map_ctypes(expr, args):
-    if isinstance(expr, sm.functions.elementary.trigonometric.TrigonometricFunction):
+    if isinstance(expr, (sm.functions.elementary.trigonometric.TrigonometricFunction,
+                         sm.functions.elementary.hyperbolic.HyperbolicFunction)):
         (args, ctype) = OpenClCastUtils.promote_expressions_to_required_signature(args,
                             'ftype', ret=0)
     elif isinstance(expr, sm.Rel):
@@ -50,7 +52,7 @@ def _map_ctypes(expr, args):
     elif isinstance(expr, LogicalRelation):
         (args, ctype) = OpenClCastUtils.promote_expressions_to_required_signature(args,
                                 (None,)*len(args), ret='btype', expand=(True,)*len(args))
-    elif isinstance(expr, (ArithmeticRelation,sm.Add,sm.Mul)):
+    elif isinstance(expr, (ArithmeticRelation,sm.Add,sm.Mul,sm.Max,sm.Min,Min,Max)):
         (args, ctype) = OpenClCastUtils.promote_expressions_to_required_signature(args,
                                 (None,)*len(args), ret=0, expand=(True,)*len(args))
     elif isinstance(expr, ComplexMul):
@@ -89,6 +91,24 @@ def _map_func(csc, expr, promoted_args, ctype, reqs):
         else:
             msg='abs({})'.format(ctype)
             raise NotImplementedError(msg)
+    elif expr.func in (sm.Max, Max):
+        if basetype(ctype) in ('half', 'float', 'double'):
+            return BuiltinFunction('fmax')
+        elif basetype(ctype) in ('char', 'short', 'int', 'long'):
+            return BuiltinFunction('max')
+        else:
+            msg='max({})'.format(ctype)
+            raise NotImplementedError(msg)
+    elif expr.func in (sm.Min, Min):
+        if basetype(ctype) in ('half', 'float', 'double'):
+            return BuiltinFunction('fmin')
+        elif basetype(ctype) in ('char', 'short', 'int', 'long'):
+            return BuiltinFunction('min')
+        else:
+            msg='min({})'.format(ctype)
+            raise NotImplementedError(msg)
+    elif expr.func is sm.exp:
+        return BuiltinFunction('exp')
     elif expr.func in _func_mappings:
         return _func_mappings[expr.func]
     return expr.func
@@ -119,6 +139,9 @@ _func_mappings = {
     sm.cos: BuiltinFunction('cos'),
     sm.sin: BuiltinFunction('sin'),
     sm.tan: BuiltinFunction('tan'),
+    sm.cosh: BuiltinFunction('cosh'),
+    sm.sinh: BuiltinFunction('sinh'),
+    sm.tanh: BuiltinFunction('tanh'),
     sm.exp: BuiltinFunction('exp'),
     sm.StrictGreaterThan: OpenClLogicalGT,
     sm.StrictLessThan:    OpenClLogicalLT,
diff --git a/hysop/backend/device/codegen/symbolic/misc.py b/hysop/backend/device/codegen/symbolic/misc.py
index 8e4a8275fd557dd8ba141610aada11da78d4e863..b36cb3f4281ab188f93a4b3ff6f17be3491c071c 100644
--- a/hysop/backend/device/codegen/symbolic/misc.py
+++ b/hysop/backend/device/codegen/symbolic/misc.py
@@ -15,11 +15,11 @@ class OpenClBroadCast(TypedI, BroadCast):
             if (self.expr.components>1):
                 indices = tuple(i for j in xrange(self.factor) for i in xrange(expr.components))
                 mode=('hex' if (expr.components>4) else 'pos')
-                bc = '{}.{}{}'.format(val, 
+                bc = '({}).{}{}'.format(val, 
                         printer.typegen.vtype_access(indices[0], expr.components, mode),
                         ''.join(printer.typegen.vtype_component_adressing(i, mode) for i in indices[1:]))
             else:
-                bc = '({})({})'.format(self.ctype, val)
+                bc = '(({})({}))'.format(self.ctype, val)
         else:
             bc = val
         return bc
@@ -38,11 +38,11 @@ class OpenClExpand(TypedI, Expand):
             if (self.expr.components>1):
                 indices = tuple(i for i in xrange(expr.components) for j in xrange(self.factor))
                 mode=('hex' if (expr.components>4) else 'pos')
-                bc = '{}.{}{}'.format(val, 
+                bc = '({}).{}{}'.format(val, 
                         printer.typegen.vtype_access(indices[0], expr.components, mode),
                         ''.join(printer.typegen.vtype_component_adressing(i, mode) for i in indices[1:]))
             else:
-                bc = '({})({})'.format(self.ctype, val)
+                bc = '(({})({}))'.format(self.ctype, val)
         else:
             bc = val
         return bc
diff --git a/hysop/backend/device/device_buffer.py b/hysop/backend/device/device_buffer.py
index 861361b5b96b3c868d024149874328c1149fdf29..8f81290a74bf0332bb8ff07640d4ba578e5b171c 100644
--- a/hysop/backend/device/device_buffer.py
+++ b/hysop/backend/device/device_buffer.py
@@ -6,4 +6,4 @@ class DeviceBuffer(Buffer):
     """
     Abstract device buffer class.
     """
-    __metaclass__=ABCMeta
+    pass
diff --git a/hysop/backend/device/kernel_autotuner.py b/hysop/backend/device/kernel_autotuner.py
index 9ac7834784fe6960c8656b53c8565471583e1453..5e8b840f5e9f94ea0659d6078f5bffd02a87fd05 100644
--- a/hysop/backend/device/kernel_autotuner.py
+++ b/hysop/backend/device/kernel_autotuner.py
@@ -84,21 +84,27 @@ class KernelAutotuner(object):
         self.stats_idx    = 6
         self.src_idx      = 7
         self.src_hash_idx = 9
+        self.logs_idx     = 10
             
     def autotune(self, extra_kwds,
-            force_verbose=False, force_debug=False):
+            first_working=False, 
+            force_verbose=False, 
+            force_debug=False):
         """
         Autotune the target tunable_kernels.
 
         Parameters
         ----------
+        first_working:
+            Disable caching, build and execute first valid kernel at most one time.
         extra_kwds: dict
             Extra keywords used to tune the kernel.
         """
         tkernel          = self.tunable_kernel
         autotuner_config = self.autotuner_config
 
-        extra_kwds_hash = tkernel.hash_extra_kwds(extra_kwds)
+        extra_kwds_hash, extra_kwds_hash_logs = tkernel.hash_extra_kwds(extra_kwds)
+        check_instance(extra_kwds_hash_logs, str)
         
         self._print_header()
         results = self._reload_cache(extra_kwds_hash)
@@ -107,23 +113,25 @@ class KernelAutotuner(object):
             if self.verbose:
                 print self.indent(1)+'>Ignoring cached results, benching all kernels.'
             best_candidate = None
+        elif first_working:
+            best_candidate = None
         else:
             best_candidate = self._load_results_from_cache(tkernel, results, extra_kwds,
-                    force_verbose, force_debug)
+                    force_verbose, force_debug, extra_kwds_hash_logs)
 
         if (best_candidate is None):
             best_candidate = self._autotune_kernels(tkernel, results, extra_kwds,
-                    force_verbose, force_debug)
+                    force_verbose, force_debug, first_working, extra_kwds_hash_logs)
         
         result_keys = ('extra_parameters', 'work_load', 'global_work_size', 'local_work_size', 
-                    'program', 'kernel', 'kernel_statistics', 'kernel_src', 'kernel_name', 
-                    'src_hash')
+                        'program', 'kernel', 'kernel_statistics', 'kernel_src', 'kernel_name', 
+                        'src_hash', 'hash_logs')
         assert len(result_keys) == len(best_candidate)
         return dict(zip(result_keys, best_candidate))
 
     
     def _load_results_from_cache(self, tkernel, results, extra_kwds,
-            force_verbose, force_debug):
+            force_verbose, force_debug, extra_kwds_hash_logs):
         if (self.FULL_RESULTS_KEY not in results):
             if self.verbose:
                 print ('  >No best candidate was cached for this configuration, '
@@ -140,7 +148,8 @@ class KernelAutotuner(object):
         
         (extra_parameters, work_load, global_work_size, local_work_size, 
             prg, kernel, statistics, cached_kernel_src, 
-            cached_kernel_name, cached_src_hash) = best_candidate
+            cached_kernel_name, cached_src_hash, 
+            cached_kernel_hash_logs) = best_candidate
 
         assert prg is None
         assert kernel is None
@@ -171,15 +180,25 @@ class KernelAutotuner(object):
             msg+='\nThis might be due to an upgrade of the generated code or '
             msg+='a faulty implementation of {}.hash_extra_kwds().'
             msg=msg.format(src_hash, cached_src_hash, type(tkernel).__name__)
-            if self.STORE_FULL_KERNEL_SOURCES and (cached_kernel_src is not None):
-                cached_src = '/tmp/cached.cl'
-                tuned_src  = '/tmp/tuned.cl'
-                with open(cached_src, 'w') as f:
-                    f.write(cached_kernel_src)
-                with open(tuned_src, 'w') as f:
-                    f.write(kernel_src)
-                msg+='\nMatching cached kernel sources dumped to \'{}\'.'.format(cached_src)
-                msg+='\nCurrently tuned kernel sources dumped to \'{}\'.'.format(tuned_src)
+            if self.STORE_FULL_KERNEL_SOURCES:
+                if (cached_kernel_src is not None):
+                    cached_src = '/tmp/cached.cl'
+                    tuned_src  = '/tmp/tuned.cl'
+                    with open(cached_src, 'w') as f:
+                        f.write(cached_kernel_src)
+                    with open(tuned_src, 'w') as f:
+                        f.write(kernel_src)
+                    msg+='\nMatching cached kernel sources dumped to \'{}\'.'.format(cached_src)
+                    msg+='\nCurrently tuned kernel sources dumped to \'{}\'.'.format(tuned_src)
+                if (cached_kernel_hash_logs is not None):
+                    cached_src = '/tmp/cached_hash_logs.txt'
+                    tuned_src  = '/tmp/tuned_hash_logs.txt'
+                    with open(cached_src, 'w') as f:
+                        f.write(cached_kernel_hash_logs)
+                    with open(tuned_src, 'w') as f:
+                        f.write(extra_kwds_hash_logs)
+                    msg+='\nMatching cached kernel sources dumped to \'{}\'.'.format(cached_src)
+                    msg+='\nCurrently tuned kernel sources dumped to \'{}\'.'.format(tuned_src)
             warnings.warn(msg, CodeGeneratorWarning)
             return None
         
@@ -193,16 +212,20 @@ class KernelAutotuner(object):
                 global_work_size=global_work_size, 
                 local_work_size=local_work_size)
             
-        best_candidate[self.prg_idx] = prg
-        best_candidate[self.knl_idx] = kernel
-        best_candidate[self.src_idx] = kernel_src
+        best_candidate[self.prg_idx]  = prg
+        best_candidate[self.knl_idx]  = kernel
+        best_candidate[self.src_idx]  = kernel_src
+        best_candidate[self.logs_idx] = extra_kwds_hash_logs
         return tuple(best_candidate)
 
 
     def _autotune_kernels(self, tkernel, results, extra_kwds, 
-            force_verbose, force_debug):
+            force_verbose, force_debug, first_working, extra_kwds_hash_logs):
         autotuner_config = self.autotuner_config 
-        nruns = autotuner_config.nruns
+        if first_working:
+            nruns = 1
+        else:
+            nruns = autotuner_config.nruns
         max_candidates = autotuner_config.max_candidates
 
         bench_results = {}
@@ -218,7 +241,8 @@ class KernelAutotuner(object):
             for extra_parameters in params.iter_parameters():
                 
                 try:
-                    self.collect_kernel_infos(tkernel=tkernel, 
+                    (max_kernel_work_group_size, preferred_work_group_size_multiple) = \
+                        self.collect_kernel_infos(tkernel=tkernel, 
                             extra_parameters=extra_parameters, 
                             extra_kwds=extra_kwds)
                 except KernelGenerationError as e:
@@ -226,7 +250,9 @@ class KernelAutotuner(object):
                         sys.stderr.write(str(e)+'\n')
                     continue
 
-                work_bounds = tkernel.compute_work_bounds(extra_parameters=extra_parameters,
+                work_bounds = tkernel.compute_work_bounds(max_kernel_work_group_size=max_kernel_work_group_size,
+                                                          preferred_work_group_size_multiple=preferred_work_group_size_multiple,
+                                                          extra_parameters=extra_parameters,
                                                           extra_kwds=extra_kwds)
                 extra_param_hash = tkernel.hash_extra_parameters(extra_parameters)
                 
@@ -320,7 +346,8 @@ class KernelAutotuner(object):
                                     tuple(global_work_size), 
                                     tuple(local_work_size), 
                                     prg, kernel, statistics, 
-                                    kernel_src, kernel_name, src_hash)
+                                    kernel_src, kernel_name, 
+                                    src_hash, extra_kwds_hash_logs)
                             
                             results[run_key] = (src_hash, statistics)
                             bench_results[run_key] = candidate
@@ -333,6 +360,7 @@ class KernelAutotuner(object):
                         total_count += 1
                         abort = (max_candidates is not None) and \
                                 ((pruned_count + kept_count) >= max_candidates)
+                        abort |= (first_working and kept_count==1)
                         self._print_full_candidate(local_work_size, global_work_size, statistics, pruned,
                                                     from_cache)
                         self._print_candidate((statistics is None), from_cache, 
@@ -345,8 +373,11 @@ class KernelAutotuner(object):
                 if abort:
                     break
             if abort:
-                msg='>Achieved maximum number of configured candidates: {}'
-                msg=msg.format(max_candidates)
+                if first_working:
+                    msg='>Achieved first running kernel.'
+                else:
+                    msg='>Achieved maximum number of configured candidates: {}'
+                    msg=msg.format(max_candidates)
                 if self.verbose>1:
                     print msg
 
@@ -367,7 +398,7 @@ class KernelAutotuner(object):
                 self._print_step(step_count, '{} BEST'.format(len(candidates)), nruns)
                 for (run_key, run_params) in candidates:
                     (extra_params, work_load, global_work_size, local_work_size, 
-                            _, kernel, old_stats, _, _, _) = run_params
+                            _, kernel, old_stats, _, _, _, _) = run_params
                     self.bench_one_from_binary(kernel=kernel,
                                              target_nruns=nruns, 
                                              old_stats=old_stats,
@@ -387,7 +418,8 @@ class KernelAutotuner(object):
 
         # Export best candidate results
         if not self.STORE_FULL_KERNEL_SOURCES:
-            best_candidate[self.src_idx] = None
+            best_candidate[self.src_idx]  = None
+            best_candidate[self.logs_idx] = None
         best_candidate[self.prg_idx] = None
         best_candidate[self.knl_idx] = None
         results[self.FULL_RESULTS_KEY] = best_candidate
@@ -398,7 +430,7 @@ class KernelAutotuner(object):
     def _build_final_kernel(self, tkernel, best_candidate,
             extra_kwds):
         (extra_parameters, work_load, global_work_size, local_work_size, 
-            _, _, _, _, _, _) = best_candidate
+            _, _, _, _, _, _, _) = best_candidate
         
         global_work_size = npw.asintegerarray(global_work_size)
         local_work_size  = npw.asintegerarray(local_work_size)
@@ -674,7 +706,7 @@ class KernelAutotuner(object):
     def _print_footer(self, ellapsed, best_candidate):
         if self.verbose:
             (best_extra_params, best_work_load, best_global_size, best_local_size, 
-                    _, _, best_stats, _, _, _) = best_candidate
+                    _, _, best_stats, _, _, _, _) = best_candidate
             if self.verbose>1:
                 if ellapsed is not None:
                     self._print_separator()
diff --git a/hysop/backend/device/kernel_autotuner_config.py b/hysop/backend/device/kernel_autotuner_config.py
index f37243879d5eb2912c07b081d920158ec9827750..9d4dd7d460b69d01f28bfcb362cfa5e3ae3e784b 100644
--- a/hysop/backend/device/kernel_autotuner_config.py
+++ b/hysop/backend/device/kernel_autotuner_config.py
@@ -9,10 +9,10 @@ class KernelAutotunerConfig(object):
     __metaclass__ = ABCMeta
 
     _default_initial_runs = {
-        AutotunerFlags.ESTIMATE:   2,
-        AutotunerFlags.MEASURE:    4,
-        AutotunerFlags.PATIENT:    8,
-        AutotunerFlags.EXHAUSTIVE: 16
+        AutotunerFlags.ESTIMATE:   1,
+        AutotunerFlags.MEASURE:    2,
+        AutotunerFlags.PATIENT:    4,
+        AutotunerFlags.EXHAUSTIVE: 8
     }
 
     def __init__(self,
@@ -30,7 +30,7 @@ class KernelAutotunerConfig(object):
         dump_folder     = first_not_None(dump_folder, self.default_dump_folder())
         autotuner_flag  = first_not_None(autotuner_flag,  DEFAULT_AUTOTUNER_FLAG)
         prune_threshold = first_not_None(prune_threshold, DEFAULT_AUTOTUNER_PRUNE_THRESHOLD)
-        max_candidates  = first_not_None(max_candidates, 8)
+        max_candidates  = first_not_None(max_candidates, 4)
         verbose         = first_not_None(verbose,  2*__VERBOSE__)
         debug           = first_not_None(debug, __KERNEL_DEBUG__)
         dump_kernels    = first_not_None(dump_kernels, __KERNEL_DEBUG__)
diff --git a/hysop/backend/device/opencl/__init__.py b/hysop/backend/device/opencl/__init__.py
index bee50ebafe21f54ae7809e301ecf3492f0bdc144..f0cab904263ccf6cf22bae72903faf32ee98ada1 100644
--- a/hysop/backend/device/opencl/__init__.py
+++ b/hysop/backend/device/opencl/__init__.py
@@ -6,6 +6,7 @@ see hysop.backend.device.opencl.opencl_tools.parse_file
 see hysop.backend.device.codegen
 """
 
+import os
 import pyopencl
 import pyopencl.tools
 import pyopencl.array
@@ -15,7 +16,16 @@ import pyopencl.clrandom
 import pyopencl.elementwise
 import pyopencl.scan
 
+try:
+    # old pyopencl interface using the cffi
+    from pyopencl import cffi_cl as cl_api
+except ImportError:
+    # new interface using pybind11
+    from pyopencl import _cl as cl_api
+
+
 from hysop import __DEFAULT_PLATFORM_ID__, __DEFAULT_DEVICE_ID__
+from hysop.tools.io_utils import IO
 from hysop.backend.device import KERNEL_DUMP_FOLDER
 
 OPENCL_KERNEL_DUMP_FOLDER='{}/opencl'.format(KERNEL_DUMP_FOLDER)
@@ -49,5 +59,14 @@ clElementwise = pyopencl.elementwise
 clCharacterize = pyopencl.characterize
 """PyOpenCL characterize"""
 
-from pyopencl._cffi import ffi as cl_ffi, lib as cl_lib
-from pyopencl import cffi_cl
+if ('CLFFT_REQUEST_NOMEMALLOC' not in os.environ):
+    os.environ['CLFFT_REQUEST_NOMEMALLOC'] = '1'
+if ('CLFFT_CACHE_PATH' not in os.environ):
+    os.environ['CLFFT_CACHE_PATH'] = IO.default_cache_path() + '/clfft'
+if not os.path.isdir(os.environ['CLFFT_CACHE_PATH']):
+    try:
+        os.makedirs(os.environ['CLFFT_CACHE_PATH'])
+    except:
+        print("Could not create clfft cache directory '{}'.".format(
+            os.environ['CLFFT_CACHE_PATH']))
+
diff --git a/hysop/backend/device/opencl/autotunable_kernels/advection_dir.py b/hysop/backend/device/opencl/autotunable_kernels/advection_dir.py
index 33d77bf40cff54908504e5810066c2e9200b15ce..5a1025ebaf35cee5e3522237ad43266a41982685 100644
--- a/hysop/backend/device/opencl/autotunable_kernels/advection_dir.py
+++ b/hysop/backend/device/opencl/autotunable_kernels/advection_dir.py
@@ -54,15 +54,14 @@ class OpenClAutotunableDirectionalAdvectionKernel(OpenClAutotunableKernel):
 
         ftype = clTools.dtype_to_ctype(precision)
         name = 'directional_advection{}_{}__{}_{}_{}g__{}'.format(
-            "" if (is_bilevel is None) else "_bilevelLinear",
-            DirectionLabels[direction],
-            time_integrator.name(), ftype, cache_ghosts, abs(hash(Vr)))
-
-        vboundaries = (velocity.boundaries[0][-1], velocity.boundaries[1][-1])
-
+                "" if (is_bilevel is None) else "_bilevel_linear",
+                DirectionLabels[direction],
+                time_integrator.name(), ftype, cache_ghosts, abs(hash(Vr)))
+        
+        vboundaries = (velocity.global_lboundaries[-1], velocity.global_rboundaries[-1])
+        
         eps = npw.finfo(precision).eps
-        dt = 10*eps
-        assert (dt > +0.0), 'Precision {} cannot represent 1e-7.'.format(precision)
+        dt = velocity_cfl
 
         make_offset, offset_dtype = self.make_array_offset()
         make_strides, strides_dtype = self.make_array_strides(position.dim,
diff --git a/hysop/backend/device/opencl/autotunable_kernels/custom_symbolic.py b/hysop/backend/device/opencl/autotunable_kernels/custom_symbolic.py
index 395f5887a660e082087ce9a9f44a737705227112..1a0cd7fc43b65dd479c25572ae65906a20ee4011 100644
--- a/hysop/backend/device/opencl/autotunable_kernels/custom_symbolic.py
+++ b/hysop/backend/device/opencl/autotunable_kernels/custom_symbolic.py
@@ -20,7 +20,7 @@ from hysop.backend.device.kernel_autotuner import KernelGenerationError
         
 class OpenClAutotunableCustomSymbolicKernel(OpenClAutotunableKernel):
     """Autotunable interface for directional remeshing kernel code generators."""
-    
+
     @classmethod
     def sort_key_by_name(cls, iterator):
         """Utility to sort a dictionary by key name."""
@@ -59,7 +59,25 @@ class OpenClAutotunableCustomSymbolicKernel(OpenClAutotunableKernel):
         work_size  = npw.asarray(cshape, dtype=npw.int32).copy()[granularity:][::-1]
         kernel_dim = work_size.size
 
-        work_dim   = min(kernel_dim, self.max_device_work_dim())
+        work_dim  = min(kernel_dim, self.max_device_work_dim())
+        work_size = work_size[:work_dim] 
+        
+        DEBUG=False
+        if DEBUG:
+            print \
+            '''
+                dim:    {}
+                dir:    {}
+                prec:   {}
+                ftype:  {}
+                cshape: {}
+
+                array_dim:  {}
+                iter_shape: {}
+                work_size:  {}
+                kernel_dim: {}
+            '''.format(dim, direction, precision, ftype, cshape,
+                      array_dim, iter_shape, work_size, work_dim)
         
         min_ghosts = npw.dim_zeros(dim)
         for mg in expr_info.min_ghosts.values():
@@ -120,14 +138,14 @@ class OpenClAutotunableCustomSymbolicKernel(OpenClAutotunableKernel):
             if isinstance(obj, di.IndexedCounterTypes):
                 assert isinstance(obj, DiscreteScalarFieldView)
                 dfield = expr_info.input_dfields[obj._field]
-                mesh_info_name = '{}_mesh_info'.format(dfield.name)
+                mesh_info_name = '{}_mesh_info'.format(dfield.var_name)
                 mesh_info_vars[mesh_info_name] = self.mesh_info(mesh_info_name, dfield.mesh)
                 for (i, count) in enumerate(counts):
                     if (count==0):
                         continue
                     if (dfield in di.write_counter) and (di.write_counter[dfield][i]>0):
                         continue
-                    vname = dfield.name + '_' + str(i)
+                    vname = dfield.var_name + '_' + str(i)
                     kernel_args[vname+'_base']    = dfield.data[i].base_data
                     target_stride_args[vname+'_strides'] = make_strides(dfield.data[i].strides, dfield.dtype)
                     target_offset_args[vname+'_offset']  = make_offset(dfield.data[i].offset, dfield.dtype)
@@ -154,12 +172,12 @@ class OpenClAutotunableCustomSymbolicKernel(OpenClAutotunableKernel):
             if isinstance(obj, di.IndexedCounterTypes):
                 assert isinstance(obj, DiscreteScalarFieldView)
                 dfield = expr_info.output_dfields[obj._field]
-                mesh_info_name = '{}_mesh_info'.format(dfield.name)
+                mesh_info_name = '{}_mesh_info'.format(dfield.var_name)
                 mesh_info_vars[mesh_info_name] = self.mesh_info(mesh_info_name, dfield.mesh)
                 for (i, count) in enumerate(counts):
                     if (count==0):
                         continue
-                    vname = dfield.name + '_' + str(i)
+                    vname = dfield.var_name + '_' + str(i)
                     kernel_args[vname+'_base']    = dfield.data[i].base_data
                     target_stride_args[vname+'_strides'] = make_strides(dfield.data[i].strides, dfield.dtype)
                     target_offset_args[vname+'_offset']  = make_offset(dfield.data[i].offset, dfield.dtype)
@@ -252,7 +270,7 @@ class OpenClAutotunableCustomSymbolicKernel(OpenClAutotunableKernel):
                         continue
                     if (dfield in di.write_counter) and (di.write_counter[dfield][i]>0):
                         continue
-                    vname = dfield.name + '_' + str(i)
+                    vname = dfield.var_name + '_' + str(i)
                     args_mapping[vname+'_base'] = (arg_index, cl.MemoryObjectHolder)
                     arg_index += 1
                     if (not hardcode_arrays):
@@ -286,7 +304,7 @@ class OpenClAutotunableCustomSymbolicKernel(OpenClAutotunableKernel):
                 for (i, count) in enumerate(counts):
                     if (count==0):
                         continue
-                    vname = dfield.name + '_' + str(i)
+                    vname = dfield.var_name + '_' + str(i)
                     args_mapping[vname+'_base'] = (arg_index, cl.MemoryObjectHolder)
                     arg_index += 1
                     if (not hardcode_arrays):
@@ -437,7 +455,7 @@ class OpenClAutotunableCustomSymbolicKernel(OpenClAutotunableKernel):
     
     def hash_extra_kwds(self, extra_kwds):
         """Hash extra_kwds dictionnary for caching purposes."""
-        kwds = ('kernel_dim', 'work_dim', 'ftype', 'granularity', 'known_args')
+        kwds = ('kernel_dim', 'work_dim', 'ftype', 'granularity', 'known_args', 'compute_shape')
         return self.custom_hash(*tuple(extra_kwds[kwd] for kwd in kwds), 
                 mesh_info_vars=extra_kwds['mesh_info_vars'],
                 expr_info=extra_kwds['expr_info'])
diff --git a/hysop/backend/device/opencl/autotunable_kernels/transpose.py b/hysop/backend/device/opencl/autotunable_kernels/transpose.py
index 240134b61a75aeeeb9c1524e0d224115f48f3153..5a040be34a49a0d9021c8d83cb3297c6c267cde4 100644
--- a/hysop/backend/device/opencl/autotunable_kernels/transpose.py
+++ b/hysop/backend/device/opencl/autotunable_kernels/transpose.py
@@ -4,7 +4,7 @@ from hysop.tools.types import check_instance
 from hysop.tools.misc import upper_pow2, previous_pow2, upper_pow2_or_3
 from hysop.tools.units import bytes2str
 from hysop.constants import AutotunerFlags
-from hysop.backend.device.opencl import cl, clTools
+from hysop.backend.device.opencl import cl, clTools, clArray
 from hysop.backend.device.opencl.opencl_autotunable_kernel import OpenClAutotunableKernel
 from hysop.backend.device.codegen.kernels.transpose import TransposeKernelGenerator
 from hysop.backend.device.kernel_autotuner import KernelGenerationError
@@ -30,23 +30,25 @@ class OpenClAutotunableTransposeKernel(OpenClAutotunableKernel):
         return upper_pow2(max_tile_size)
 
     def autotune(self, is_inplace, 
-            input_field, output_field, 
+            input_buffer, output_buffer, 
             axes, hardcode_arrays,
             name=None, **kwds):
         """Autotune this kernel with specified axes, inputs and outputs."""
 
         check_instance(axes, tuple, values=int)
         check_instance(is_inplace, bool)
-        self.check_cartesian_fields(input_field, output_field, 
-                check_res=False, check_size=True)
-            
-        dim   = input_field.domain.dim
-        dtype = input_field.dtype
-        ctype = clTools.dtype_to_ctype(dtype)
-        shape = input_field.resolution[::-1]
+        check_instance(input_buffer,  clArray.Array)
+        check_instance(output_buffer, clArray.Array)
 
-        assert input_field.nb_components == 1
-        assert output_field.nb_components == 1
+        assert input_buffer.ndim  == output_buffer.ndim
+        assert input_buffer.size  == output_buffer.size
+        assert input_buffer.dtype == output_buffer.dtype
+        
+        dim   = input_buffer.ndim
+        size  = input_buffer.size
+        shape = npw.asintarray(input_buffer.shape[::-1])
+        dtype = input_buffer.dtype
+        ctype = clTools.dtype_to_ctype(dtype)
         
         # check if the permutation is valid
         assert dim>=2
@@ -61,11 +63,11 @@ class OpenClAutotunableTransposeKernel(OpenClAutotunableKernel):
             msg='Inplace was specified but this is only possible for 2D square arrays.'
             if not can_compute_inplace:
                 raise ValueError(msg)
-            assert input_field.dfield == output_field.dfield
+            assert (input_buffer.data == output_buffer.data)
         
         # get vector size for strides
         make_offset, offset_dtype = self.make_array_offset()
-        make_strides, strides_dtype = self.make_array_strides(input_field.dim, 
+        make_strides, strides_dtype = self.make_array_strides(dim, 
                 hardcode_arrays=hardcode_arrays)
         
         kernel_args = {}
@@ -74,23 +76,23 @@ class OpenClAutotunableTransposeKernel(OpenClAutotunableKernel):
         target_args = known_args if hardcode_arrays else kernel_args
 
         if is_inplace:
-            kernel_args['inout_base']    = output_field.sbuffer.data
-            target_args['inout_strides'] = make_strides(output_field.sbuffer.strides, output_field.dtype)
-            target_args['inout_offset']  = make_offset(output_field.sbuffer.offset, output_field.dtype)
-            isolation_params['inout_base'] = dict(count=output_field.npoints, 
-                    dtype=output_field.dtype, range=slice(output_field.npoints))
+            kernel_args['inout_base']    = output_buffer.base_data
+            target_args['inout_strides'] = make_strides(output_buffer.strides, output_buffer.dtype)
+            target_args['inout_offset']  = make_offset(output_buffer.offset, output_buffer.dtype)
+            isolation_params['inout_base'] = dict(count=output_buffer.size, 
+                    dtype=output_buffer.dtype, range=slice(output_buffer.size))
         else:
-            kernel_args['in_base']     = input_field.sbuffer.base_data
-            target_args['in_strides']  = make_strides(input_field.sbuffer.strides, input_field.dtype)
-            target_args['in_offset']   = make_offset(input_field.sbuffer.offset, input_field.dtype)
-            isolation_params['in_base'] = dict(count=input_field.npoints, dtype=input_field.dtype, 
-                                                    range=slice(input_field.npoints))
+            kernel_args['in_base']     = input_buffer.base_data
+            target_args['in_strides']  = make_strides(input_buffer.strides, input_buffer.dtype)
+            target_args['in_offset']   = make_offset(input_buffer.offset, input_buffer.dtype)
+            isolation_params['in_base'] = dict(count=input_buffer.size, dtype=input_buffer.dtype, 
+                                                    range=slice(input_buffer.size))
 
-            kernel_args['out_base']    = output_field.sbuffer.base_data
-            target_args['out_strides'] = make_strides(output_field.sbuffer.strides, output_field.dtype)
-            target_args['out_offset']  = make_offset(output_field.sbuffer.offset, output_field.dtype)
-            isolation_params['out_base'] = dict(count=output_field.npoints, 
-                                                    dtype=output_field.dtype, fill=0)
+            kernel_args['out_base']    = output_buffer.base_data
+            target_args['out_strides'] = make_strides(output_buffer.strides, output_buffer.dtype)
+            target_args['out_offset']  = make_offset(output_buffer.offset, output_buffer.dtype)
+            isolation_params['out_base'] = dict(count=output_buffer.size, 
+                                                    dtype=output_buffer.dtype, fill=0)
         
         if (name is None):
             name = 'transpose_{}_[{}]_{}'.format(ctype,
@@ -111,8 +113,6 @@ class OpenClAutotunableTransposeKernel(OpenClAutotunableKernel):
                 axes=axes, 
                 dtype=dtype, 
                 ctype=ctype,
-                input_field=input_field, 
-                output_field=output_field, 
                 shape=shape,
                 tile_indices=tile_indices, 
                 work_dim=work_dim,
diff --git a/hysop/backend/device/opencl/clpeak.py b/hysop/backend/device/opencl/clpeak.py
index 657305c8a9d31b49d957af2f0c4e413de0010720..1cbba49a1b34c66119a67918164e8f8ef94324ae 100644
--- a/hysop/backend/device/opencl/clpeak.py
+++ b/hysop/backend/device/opencl/clpeak.py
@@ -14,7 +14,7 @@ from hysop.backend.hardware.hwinfo import HardwareStatistics
 
 class ClPeakInfo(object):
     __FNULL = open(os.devnull, 'w')
-    __CMD_TIME_OUT = 30 # 30s timeout for clpeak calls
+    __CMD_TIME_OUT = 60 # 60s timeout for clpeak calls
     __clpeak_bandwidth_units = {
                 'bps':  1e00,
                 'kbps': 1e03,
diff --git a/hysop/backend/device/opencl/opencl_allocator.py b/hysop/backend/device/opencl/opencl_allocator.py
index d1cb7dc7eb717a3567f0872e812983dbd28702da..44d8f8b0c7b6ee5fc8232ac7042cdacb156d9e00 100644
--- a/hysop/backend/device/opencl/opencl_allocator.py
+++ b/hysop/backend/device/opencl/opencl_allocator.py
@@ -1,7 +1,7 @@
 
 from abc import ABCMeta, abstractmethod
 from hysop.deps import np
-from hysop.backend.device.opencl import cl
+from hysop.backend.device.opencl import cl, cl_api
 from hysop.core.memory.allocator import AllocatorBase
 from hysop.backend.device.opencl.opencl_buffer import OpenClBuffer
 
@@ -13,10 +13,13 @@ class OpenClAllocator(AllocatorBase):
 
     def __init__(self, queue, mem_flags=cl.mem_flags.READ_WRITE, verbose=None):
         super(OpenClAllocator, self).__init__(verbose=verbose)
+        if (queue.device.type == cl.device_type.CPU):
+            # we assume zero copy capabilitiy for CPU devices
+            mem_flags |= cl.mem_flags.ALLOC_HOST_PTR
         self._queue  = queue
         self._mem_flags = mem_flags
         if mem_flags & cl.mem_flags.COPY_HOST_PTR:
-            raise ValueError('pyopencl.mem_flags.COPY_HOST_PTR cannot be passed ')
+            raise ValueError('pyopencl.mem_flags.COPY_HOST_PTR cannot be passed for an allocator.')
         self._max_alloc_size = queue.device.max_mem_alloc_size
 
     def max_alloc_size(self):
@@ -85,8 +88,7 @@ class OpenClImmediateAllocator(OpenClAllocator):
         buf = OpenClBuffer(context=self.context, mem_flags=self.mem_flags, size=nbytes)
 
         try:
-            from pyopencl.cffi_cl import _enqueue_write_buffer
-            _enqueue_write_buffer(
+            cl_api._enqueue_write_buffer(
                     self.queue, buf,
                     self._zero[:min(len(self._zero), nbytes)],
                     is_blocking=True)
diff --git a/hysop/backend/device/opencl/opencl_array.py b/hysop/backend/device/opencl/opencl_array.py
index 931efa11adca62550f1e4de82d41fe8b0ba0bb59..39cf0e7a57a41d90c22917ccb5f7c88e89b5a1f5 100644
--- a/hysop/backend/device/opencl/opencl_array.py
+++ b/hysop/backend/device/opencl/opencl_array.py
@@ -139,7 +139,9 @@ class OpenClArray(Array):
         Returns a HostArray, view or copy of this array.
         """
         queue = self.backend.check_queue(queue)
-        if self.flags.forc:
+        if self.size==0:
+            return None
+        elif self.flags.forc:
             host_array = self._call('get', queue=queue, ary=ary, async=async)
         else:
             from hysop.backend.device.opencl.opencl_copy_kernel_launchers import \
@@ -390,7 +392,7 @@ class OpenClArray(Array):
                 evt.wait()
 
     def __setitem__(self, subscript, value, **kwds):
-        if slices_empty(subscript, self.shape):
+        if any( (s==0) for s in self[subscript].shape ):
             return
         self.setitem(subscript=subscript, value=value, **kwds)
     
diff --git a/hysop/backend/device/opencl/opencl_array_backend.py b/hysop/backend/device/opencl/opencl_array_backend.py
index 2b72adb314c799290bc375d6b55290251001d85e..b9f70a2ffa7e618c0b43f0275113d1782b1bf4dd 100644
--- a/hysop/backend/device/opencl/opencl_array_backend.py
+++ b/hysop/backend/device/opencl/opencl_array_backend.py
@@ -351,7 +351,8 @@ class OpenClArrayBackend(ArrayBackend):
             raise RuntimeError(msg)
         
         if (queue is None):
-            cl_env    = cl_env or get_or_create_opencl_env()
+            if (cl_env is None):
+                cl_env = get_or_create_opencl_env()
             context   = cl_env.context
             queue     = cl_env.default_queue
             allocator = allocator or cl_env.allocator
@@ -393,6 +394,9 @@ class OpenClArrayBackend(ArrayBackend):
         self._default_queue = queue
         self._host_array_backend = host_array_backend
         self._cl_env = cl_env
+        
+        from hysop.backend.device.opencl.opencl_elementwise import OpenClElementwiseKernelGenerator
+        self._kernel_generator = OpenClElementwiseKernelGenerator(cl_env=cl_env)
 
     def get_default_queue(self):
         return self._default_queue
@@ -1569,54 +1573,57 @@ class OpenClArrayBackend(ArrayBackend):
         queue = queue = self.check_queue(queue)
         return self._call(a.handle.copy, queue=queue)
 
-# Filling
+    # Filling
     def fill(self, a, value, queue=None):
         """
         Fill the array with given value
         """
-        queue = queue = self.check_queue(queue)
+        queue = self.check_queue(queue)
         if a.flags.forc:
             # only c-contiguous arrays are handled by pyopencl
             a.handle.fill(value=value, queue=queue)
         else:
-            # else we fill on cpu and copy it to device
-            b = a.backend.host_array_backend.full(shape=a.shape, dtype=a.dtype, fill_value=value)
-            a.copy_from(b, queue=queue)
+            # else we have to use hysop opencl codegen backend
+            from hysop.symbolic.relational import Assignment
+            a, = self._kernel_generator.arrays_to_symbols(a)
+            expr = Assignment(a, value)
+            self._kernel_generator.elementwise_kernel('fill', expr, 
+                                queue=queue, call_only_once=True)
 
 # Ones and zeros
-    def empty(self, shape, dtype=HYSOP_REAL, order=default_order, queue=None):
+    def empty(self, shape, dtype=HYSOP_REAL, order=default_order, queue=None, min_alignment=None):
         """
         Return a new array of given shape and type, without initializing entries.
         If queue is specified, the queue becomes the default queue, else 
         backend.default_queue is used instead.
         """
-        return self.array(shape=shape, dtype=dtype, order=order, queue=queue)
+        return self.array(shape=shape, dtype=dtype, order=order, queue=queue, min_alignment=min_alignment)
     
-    def full(self, shape, fill_value, dtype=HYSOP_REAL, order=default_order, queue=None):
+    def full(self, shape, fill_value, dtype=HYSOP_REAL, order=default_order, queue=None, min_alignment=None):
         """
         Return a new array of given shape and type, filled with fill_value.
         Queue is set as default queue.
         """
-        a = self.empty(shape=shape, dtype=dtype, order=order, queue=queue)
+        a = self.empty(shape=shape, dtype=dtype, order=order, queue=queue, min_alignment=min_alignment)
         self.fill(a=a, value=fill_value, queue=queue)
         return a
-    def zeros(self, shape, dtype=HYSOP_REAL, order=default_order, queue=None):
+    def zeros(self, shape, dtype=HYSOP_REAL, order=default_order, queue=None, min_alignment=None):
         """
         Return a new array of given shape and type, filled with zeros.
         Queue is set as default queue.
         """
         return self.full(shape=shape, dtype=dtype, order=order, queue=queue, 
-                 fill_value=0)
-    def ones(self, shape, dtype=HYSOP_REAL, order=default_order, queue=None):
+                 fill_value=0, min_alignment=min_alignment)
+    def ones(self, shape, dtype=HYSOP_REAL, order=default_order, queue=None, min_alignment=None):
         """
         Return a new array of given shape and type, filled with ones.
         Queue is set as default queue.
         """
         return self.full(shape=shape, dtype=dtype, order=order, queue=queue, 
-                 fill_value=1)
+                 fill_value=1, min_alignment=min_alignment)
 
 
-    def empty_like(self, a, shape=None, dtype=None, order=None, subok=True, queue=None):
+    def empty_like(self, a, shape=None, dtype=None, order=None, subok=True, queue=None, min_alignment=None):
         """
         Return a new array with the same shape and type as a given array.
         Queue is set as default queue.
@@ -1625,30 +1632,30 @@ class OpenClArrayBackend(ArrayBackend):
         shape = first_not_None(shape, a.shape)
         order = first_not_None(order, getattr(a, 'order', default_order))
         return self.array(shape=shape, queue=queue, 
-                            dtype=dtype, order=order)
-    def full_like(self, a, fill_value, dtype=None, order=None, subok=True, queue=None):
+                            dtype=dtype, order=order, min_alignment=min_alignment)
+    def full_like(self, a, fill_value, dtype=None, order=None, subok=True, queue=None, min_alignment=None):
         """
         Return a new array with the same shape and type as a given array.
         Queue is set as default queue.
         """
         a = self.empty_like(a=a, dtype=dtype, order=order, subok=subok, 
-                            queue=queue)
+                            queue=queue, min_alignment=min_alignment)
         self.fill(a, value=fill_value, queue=queue)
         return a
-    def zeros_like(self, a, dtype=None, order=None, subok=True, queue=None):
+    def zeros_like(self, a, dtype=None, order=None, subok=True, queue=None, min_alignment=None):
         """
         Return an array of zeros with the same shape and type as a given array.
         Queue is set as default queue.
         """
         return self.full_like(a=a,fill_value=0,dtype=dtype,order=order,subok=subok,
-                                 queue=queue)
-    def ones_like(self, a, dtype=None, order=None, subok=True, queue=None):
+                                 queue=queue, min_alignment=min_alignment)
+    def ones_like(self, a, dtype=None, order=None, subok=True, queue=None, min_alignment=None):
         """
         Return an array of ones with the same shape and type as a given array.
         Queue is set as default queue.
         """
         return self.full_like(a=a,fill_value=1,dtype=dtype,order=order,subok=subok,
-                                queue=queue)
+                                queue=queue, min_alignment=min_alignment)
 
     def arange(self, *args, **kargs):
         """
@@ -3156,7 +3163,7 @@ class OpenClArrayBackend(ArrayBackend):
         """
         Compute the arithmetic mean along the specified axis.
         """
-        return a.sum(a=a, axis=axis, dtype=dtype, out=out, queue=queue) / float(a.size)
+        return a.sum(axis=axis, dtype=dtype, out=out, queue=queue) / float(a.size)
 
     def std(self, a, axis=None, dtype=None, out=None, ddof=0, queue=None):
         """
diff --git a/hysop/backend/device/opencl/opencl_autotunable_kernel.py b/hysop/backend/device/opencl/opencl_autotunable_kernel.py
index 1a4f8c71f37afc1cbac85f858c2c2bc2004f4e39..346c9675316c73c0c5ac1c562505e975985295e6 100644
--- a/hysop/backend/device/opencl/opencl_autotunable_kernel.py
+++ b/hysop/backend/device/opencl/opencl_autotunable_kernel.py
@@ -85,7 +85,7 @@ class OpenClAutotunableKernel(AutotunableKernel):
             global_work_size, local_work_size,
             args_mapping, args_list,
             program, kernel, kernel_name, kernel_src,
-            kernel_statistics, src_hash):
+            kernel_statistics, src_hash, hash_logs):
         """
         Post treatment callback for autotuner results.
         Transform autotuner results in user friendly kernel wrappers.
@@ -106,6 +106,7 @@ class OpenClAutotunableKernel(AutotunableKernel):
         check_instance(kernel_name, str)
         check_instance(kernel_statistics, OpenClKernelStatistics)
         check_instance(src_hash, str)
+        check_instance(hash_logs, str)
 
         isolation_params = extra_kwds['isolation_params']
 
diff --git a/hysop/backend/device/opencl/opencl_buffer.py b/hysop/backend/device/opencl/opencl_buffer.py
index c051863114951e6b9ca546a1233ecbc7441bf709..322e2d86c001e75680cbdeb260577a96a3bc6584 100644
--- a/hysop/backend/device/opencl/opencl_buffer.py
+++ b/hysop/backend/device/opencl/opencl_buffer.py
@@ -1,6 +1,6 @@
 from hysop.core.memory.buffer import PooledBuffer
 from hysop.backend.device.device_buffer import DeviceBuffer
-from hysop.backend.device.opencl import cl, cl_ffi, cl_lib, cffi_cl
+from hysop.backend.device.opencl import cl
 
 class OpenClBuffer(DeviceBuffer, cl.Buffer):
     """
@@ -61,6 +61,9 @@ class OpenClBuffer(DeviceBuffer, cl.Buffer):
         super(OpenClBuffer, self).release()
 
 class OpenClPooledBuffer(PooledBuffer, cl.MemoryObjectHolder):
+    def __init__(self, *args, **kwds):
+        super(OpenClPooledBuffer, self).__init__(*args, **kwds)
+
     def get_ptr(self):
         return self._bufview.ptr
     ptr = property(get_ptr)
diff --git a/hysop/backend/device/opencl/opencl_copy_kernel_launchers.py b/hysop/backend/device/opencl/opencl_copy_kernel_launchers.py
index 02c08639358c7be00d0580e1f8046e90b9d0ab54..ef94be08d0e902db33f14ac8f53547188af8a4cf 100644
--- a/hysop/backend/device/opencl/opencl_copy_kernel_launchers.py
+++ b/hysop/backend/device/opencl/opencl_copy_kernel_launchers.py
@@ -1,6 +1,7 @@
 
 from hysop import vprint, dprint, __KERNEL_DEBUG__, __TRACE_KERNELS__
 from hysop.deps import np
+from hysop.constants import Backend
 from hysop.tools.decorators import debug
 from hysop.tools.types import check_instance, first_not_None
 from hysop.tools.misc import prod
@@ -11,6 +12,7 @@ from hysop.backend.device.opencl import cl, clArray
 from hysop.backend.device.opencl.opencl_kernel_launcher import OpenClKernelLauncher
 from hysop.backend.device.opencl.opencl_kernel_statistics import OpenClKernelStatistics
 
+
 class OpenClCopyKernelLauncher(OpenClKernelLauncher):
     """Interface to non-blocking OpenCL copy kernels."""
     
@@ -52,7 +54,8 @@ class OpenClCopyKernelLauncher(OpenClKernelLauncher):
             print '  '+self._apply_msg
         queue = first_not_None(queue, self._default_queue)
         check_instance(queue, cl.CommandQueue)
-        evt = cl.enqueue_copy(queue=queue, **self._enqueue_copy_kwds)
+        evt = cl.enqueue_copy(queue=queue, wait_for=wait_for,
+                **self._enqueue_copy_kwds)
         return evt
 
     def global_size_configured(self):
@@ -211,9 +214,14 @@ class OpenClCopyBufferRectLauncher(OpenClCopyKernelLauncher):
     """
     Non-blocking OpenCL copy kernel between host buffers and/or opencl device 
     rectangle subregions of buffers (OpenCL 1.1 and newer).
+
+    Supports n-dimensional strided arrays with dimension greater than 3 
+    via iterating over 3D subregions.
     """
-    def __init__(self, varname, src, dst, region, 
-            src_origin, dst_origin, src_pitches, dst_pitches, **kwds):
+    def __init__(self, varname, src, dst, 
+            copy_region, copy_src_origin, copy_dst_origin, copy_src_pitches, copy_dst_pitches, 
+            iter_region=None, iter_src_origin=None, iter_dst_origin=None, iter_src_pitches=None, iter_dst_pitches=None, 
+            **kwds):
         """
         Initialize a (HOST <-> DEVICE) or a (DEVICE <-> DEVICE) rectangle
         subregions copy kernel.
@@ -226,33 +234,58 @@ class OpenClCopyBufferRectLauncher(OpenClCopyKernelLauncher):
             The source buffer.
         dst: cl.MemoryObjectHolder or np.ndarray
             The destination buffer.
-        region: tuple of ints
+        copy_region: tuple of ints
             The 3D region to copy in terms of bytes for the 
             first dimension and of elements for the two last dimensions.
-        src_origin: tuple of ints
+        copy_src_origin: tuple of ints
             The 3D offset in number of elements of the region associated with src buffer. 
             The final src offset in bytes is computed from src_origin and src_pitch.
-        dst_origin: tuple of ints
+        copy_dst_origin: tuple of ints
             The 3D offset in number of elements of the region associated with dst buffer. 
             The final dst offset in bytes is computed from dst_origin and dst_pitch.
-        src_pitches: tuple of ints
+        copy_src_pitches: tuple of ints
             The 2D pitches used to compute src offsets in bytes for 
             the second and the third dimension.
-        dst_pitches: tuple of ints
+        copy_dst_pitches: tuple of ints
             The 2D pitches used to compute dst offsets in bytes for 
             the second and the third dimension.
+        iter_region: tuple of ints
+            The n-dimensional region to iterate if the copied region dimension is greater than 3.
+        iter_src_origin: tuple of ints
+            The n-dimensional src array origin if the copied region dimension is greater than 3.
+        iter_dst_origin: tuple of ints
+            The n-dimensional dst array origin if the copied region dimension is greater than 3.
+        iter_src_pitches: tuple of ints
+            The n-dimensional src array pitches if the copied region dimension is greater than 3.
+        iter_dst_pitches: tuple of ints
+            The n-dimensional dst array pitches if the copied region dimension is greater than 3.
         kwds: dict
             Base class arguments
         """
+        iter_region      = first_not_None(iter_region,      ())
+        iter_src_origin  = first_not_None(iter_src_origin,  ())
+        iter_dst_origin  = first_not_None(iter_dst_origin,  ())
+        iter_src_pitches = first_not_None(iter_src_pitches, ())
+        iter_dst_pitches = first_not_None(iter_dst_pitches, ())
+
         check_instance(src, (cl.MemoryObjectHolder, np.ndarray))
         check_instance(dst, (cl.MemoryObjectHolder, np.ndarray))
-        check_instance(src_origin,  tuple, values=(int, np.integer), size=3)
-        check_instance(dst_origin,  tuple, values=(int, np.integer), size=3)
-        check_instance(src_pitches, tuple, values=(int, np.integer), size=2)
-        check_instance(dst_pitches, tuple, values=(int, np.integer), size=2)
+
+        check_instance(copy_region,      tuple, values=(int, np.integer), size=3)
+        check_instance(copy_src_origin,  tuple, values=(int, np.integer), size=3)
+        check_instance(copy_dst_origin,  tuple, values=(int, np.integer), size=3)
+        check_instance(copy_src_pitches, tuple, values=(int, np.integer), size=2)
+        check_instance(copy_dst_pitches, tuple, values=(int, np.integer), size=2)
+        
+        n = len(iter_region)
+        check_instance(iter_region,      tuple, values=(int, np.integer), size=n)
+        check_instance(iter_src_origin,  tuple, values=(int, np.integer), size=n)
+        check_instance(iter_dst_origin,  tuple, values=(int, np.integer), size=n)
+        check_instance(iter_src_pitches, tuple, values=(int, np.integer), size=n)
+        check_instance(iter_dst_pitches, tuple, values=(int, np.integer), size=n)
         
         enqueue_copy_kwds = {}
-        enqueue_copy_kwds['region'] = region
+        enqueue_copy_kwds['region'] = copy_region
 
         if isinstance(src, np.ndarray) and \
            isinstance(dst, np.ndarray):
@@ -260,22 +293,28 @@ class OpenClCopyBufferRectLauncher(OpenClCopyKernelLauncher):
             raise RuntimeError(msg)
         elif isinstance(src, cl.MemoryObjectHolder) and \
              isinstance(dst, cl.MemoryObjectHolder):
-            enqueue_copy_kwds['src_origin']  = src_origin
-            enqueue_copy_kwds['src_pitches'] = src_pitches
-            enqueue_copy_kwds['dst_origin']  = dst_origin
-            enqueue_copy_kwds['dst_pitches'] = dst_pitches
+            enqueue_copy_kwds['src_origin']  = copy_src_origin
+            enqueue_copy_kwds['src_pitches'] = copy_src_pitches
+            enqueue_copy_kwds['dst_origin']  = copy_dst_origin
+            enqueue_copy_kwds['dst_pitches'] = copy_dst_pitches
+            src_origin_kwd = 'src_origin'
+            dst_origin_kwd = 'dst_origin'
         elif isinstance(src, cl.MemoryObjectHolder) and \
              isinstance(dst, np.ndarray):
-            enqueue_copy_kwds['host_origin']  = dst_origin
-            enqueue_copy_kwds['host_pitches'] = dst_pitches
-            enqueue_copy_kwds['buffer_origin']  = src_origin
-            enqueue_copy_kwds['buffer_pitches'] = src_pitches
+            enqueue_copy_kwds['host_origin']    = copy_dst_origin
+            enqueue_copy_kwds['host_pitches']   = copy_dst_pitches
+            enqueue_copy_kwds['buffer_origin']  = copy_src_origin
+            enqueue_copy_kwds['buffer_pitches'] = copy_src_pitches
+            src_origin_kwd = 'buffer_origin'
+            dst_origin_kwd = 'host_origin'
         elif isinstance(src, np.ndarray) and \
              isinstance(dst, cl.MemoryObjectHolder):
-            enqueue_copy_kwds['host_origin']  = src_origin
-            enqueue_copy_kwds['host_pitches'] = src_pitches
-            enqueue_copy_kwds['buffer_origin']  = dst_origin
-            enqueue_copy_kwds['buffer_pitches'] = dst_pitches
+            enqueue_copy_kwds['host_origin']    = copy_src_origin
+            enqueue_copy_kwds['host_pitches']   = copy_src_pitches
+            enqueue_copy_kwds['buffer_origin']  = copy_dst_origin
+            enqueue_copy_kwds['buffer_pitches'] = copy_dst_pitches
+            src_origin_kwd = 'host_origin'
+            dst_origin_kwd = 'buffer_origin'
         else:
             msg='The impossible happened.\n *src={}\n *dst={}'
             msg=msg.format(type(src), type(dst))
@@ -286,11 +325,54 @@ class OpenClCopyBufferRectLauncher(OpenClCopyKernelLauncher):
                 'host' if isinstance(src, np.ndarray) else 'device',
                 'host' if isinstance(dst, np.ndarray) else 'device')
         apply_msg='{}<<<{}>>>()'
-        apply_msg=apply_msg.format(name, region)
+        apply_msg=apply_msg.format(name, copy_region)
+        
+        # if iteration is required, we redefine __call__
+        if (n>0):
+            apply_msg += ' iterated over ndindex {}'.format(iter_region)
+            assert src_origin_kwd in enqueue_copy_kwds
+            assert dst_origin_kwd in enqueue_copy_kwds
+            src_origin = enqueue_copy_kwds.pop(src_origin_kwd)
+            dst_origin = enqueue_copy_kwds.pop(dst_origin_kwd)
 
         super(OpenClCopyBufferRectLauncher, self).__init__(dst=dst, src=src, 
                 enqueue_copy_kwds=enqueue_copy_kwds, 
                 name=name, apply_msg=apply_msg, **kwds)
+            
+        if (n>0):
+            def call(queue=None, wait_for=None, 
+                    iter_region=iter_region,
+                    iter_src_origin=iter_src_origin,
+                    iter_dst_origin=iter_dst_origin,
+                    iter_src_pitches=iter_src_pitches,
+                    iter_dst_pitches=iter_dst_pitches,
+                    **kwds):
+                if __KERNEL_DEBUG__ or __TRACE_KERNELS__:
+                    print '  '+self._apply_msg
+                queue = first_not_None(queue, self._default_queue)
+                check_instance(queue, cl.CommandQueue)
+
+                for idx in npw.ndindex(*iter_region):
+                    src_byte_offset = npw.dot(npw.add(iter_src_origin, idx), iter_src_pitches)
+                    dst_byte_offset = npw.dot(npw.add(iter_dst_origin, idx), iter_dst_pitches)
+                    _src_origin = (src_origin[0]+src_byte_offset,) + src_origin[1:]
+                    _dst_origin = (dst_origin[0]+dst_byte_offset,) + dst_origin[1:]
+                    enqueue_copy_kwds[src_origin_kwd] = _src_origin
+                    enqueue_copy_kwds[dst_origin_kwd] = _dst_origin
+                    evt = cl.enqueue_copy(queue=queue, wait_for=wait_for,
+                            **enqueue_copy_kwds)
+                    wait_for = None
+                return evt
+            self.call = call
+        else:
+            self.call = None
+
+    def __call__(self, *args, **kwds):
+        if (self.call is None):
+            return super(OpenClCopyBufferRectLauncher, self).__call__(*args, **kwds)
+        else:
+            return self.call(*args, **kwds)
+        
 
     @classmethod
     def _format_slices(cls, a, slices):
@@ -416,7 +498,6 @@ class OpenClCopyBufferRectLauncher(OpenClCopyKernelLauncher):
         msg0+='\n *Slices conversions were:'
         msg0+='\n   src_slices: {}'
         msg0+='\n   dst_slices: {}'
-        msg0+='\n'
         msg0 = msg0.format(src.shape, src.dtype, src_slices, 
                            dst.shape, dst.dtype, dst_slices,
                            '{}', '{}')
@@ -428,50 +509,132 @@ class OpenClCopyBufferRectLauncher(OpenClCopyKernelLauncher):
         msg0 = msg0.format(src_slices, dst_slices)
 
         if (src_bytes != dst_bytes):
-            msg =' >Error: byte size mismatch between source and destination slices:'
-            msg+='\n   src_slices: nelems={}, dtype={}, bytes={} ({}B)'
-            msg+='\n   dst_slices: nelems={}, dtype={}, bytes={} ({}B)'
-            msg =msg.format(src_nelems, src_dtype, bytes2str(src_bytes), src_bytes, 
-                            dst_nelems, dst_dtype, bytes2str(dst_bytes), dst_bytes)
-            raise ValueError(msg0+msg)
+            msg0+='\n >Error: byte size mismatch between source and destination slices:'
+        else:
+            msg0+='\n *Data types and byte count:'
+        msg0+='\n   src: nelems={}, dtype={}, bytes={} ({}B)'
+        msg0+='\n   dst: nelems={}, dtype={}, bytes={} ({}B)'
+        msg0=msg0.format(src_nelems, src_dtype, bytes2str(src_bytes), src_bytes, 
+                        dst_nelems, dst_dtype, bytes2str(dst_bytes), dst_bytes)
+        if (src_bytes != dst_bytes):
+            raise ValueError(msg0)
 
         src_data, src_region, src_origin, src_pitches = cls._compute_region(src, src_indices)
         dst_data, dst_region, dst_origin, dst_pitches = cls._compute_region(dst, dst_indices)
 
         if (src_region != dst_region).any():
-            msg =' >Error: mismatch between source and destination regions:'
-            msg+='\n   src_region: {}'
-            msg+='\n   dst_region: {}'
-            msg =msg.format(src_region, dst_region)
-            raise ValueError(msg0+msg)
+            msg0 +='\n >Error: mismatch between source and destination regions:'
+        else:
+            msg0 += '\n *Determined regions:'
+        msg0+='\n   src: region={}, origin={}, pitches={}'
+        msg0+='\n   dst: region={}, origin={}, pitches={}'
+        msg0=msg0.format(src_region, src_origin, src_pitches,
+                         dst_region, dst_origin, dst_pitches)
+        if (src_region != dst_region).any():
+            raise ValueError(msg0)
         region = src_region
 
         if (region<=0).any():
-            msg =' >Error: region is ill-formed or zero-sized:'
+            msg ='\n >Error: region is ill-formed or zero-sized:'
             msg+='\n   region: {}'
-            msg =msg.format(region)
+            msg = msg.format(region)
             raise ValueError(msg0+msg)
 
-        copy_dims = src_region.size
+        total_dims = src_region.size
+        copy_dims = min(total_dims, 3)
+        iter_dims = total_dims - copy_dims
         assert copy_dims > 0
-        if (copy_dims > 3):
-            msg=' >Error: clEnqueueCopyBufferRect only support up to 3D regions.'
-            msg+='\n   src_region: {}'
-            msg+='\n   dst_region: {}'
-            msg =msg.format(src_region, dst_region)
+        assert iter_dims >= 0
     
         zero, one = np.int32(0), np.int32(1)
-        _region, _src_origin, _dst_origin = [one]*3, [zero]*3, [zero]*3
-        _src_pitches, _dst_pitches = [zero]*2, [zero]*2
-
-        _region[:copy_dims]        = region[::-1]
-        _src_origin[:copy_dims]    = src_origin[::-1]
-        _dst_origin[:copy_dims]    = dst_origin[::-1]
-        _src_pitches[:copy_dims-1] = src_pitches[::-1]
-        _dst_pitches[:copy_dims-1] = dst_pitches[::-1]
+        copy_region = [one]*3
+        copy_src_origin, copy_dst_origin = [zero]*3, [zero]*3
+        copy_src_pitches, copy_dst_pitches = [zero]*2, [zero]*2
+
+        copy_region[:copy_dims]        = region[::-1][:copy_dims]
+        copy_src_origin[:copy_dims]    = src_origin[::-1][:copy_dims]
+        copy_dst_origin[:copy_dims]    = dst_origin[::-1][:copy_dims]
+        copy_src_pitches[:copy_dims-1] = src_pitches[::-1][:copy_dims-1]
+        copy_dst_pitches[:copy_dims-1] = dst_pitches[::-1][:copy_dims-1]
+
+        copy_region      = tuple(copy_region) 
+        copy_src_origin  = tuple(copy_src_origin)
+        copy_dst_origin  = tuple(copy_dst_origin)
+        copy_src_pitches = tuple(copy_src_pitches)
+        copy_dst_pitches = tuple(copy_dst_pitches)
         
-        return OpenClCopyBufferRectLauncher(varname=varname, 
-                src=src_data, dst=dst_data, region=tuple(_region),
-                src_origin=tuple(_src_origin), dst_origin=tuple(_dst_origin),
-                src_pitches=tuple(_src_pitches), dst_pitches=tuple(_dst_pitches))
+        iter_region      = tuple(region[:iter_dims])
+        iter_src_origin  = tuple(src_origin[:iter_dims])
+        iter_dst_origin  = tuple(dst_origin[:iter_dims])
+        iter_src_pitches = tuple(src_pitches[:iter_dims])
+        iter_dst_pitches = tuple(dst_pitches[:iter_dims])
+
+        msg0+='\n *Dimensions:'
+        msg0+='\n   total: {}'
+        msg0+='\n   copy:  {}'
+        msg0+='\n   iter:  {}'
+        msg0=msg0.format(total_dims, copy_dims, iter_dims)
+
+        msg0+='\n *enqueue_copy kernel arguments:'
+        msg0+='\n   region:     {}'
+        msg0+='\n   src: origin={}, pitches={}'
+        msg0+='\n   dst: origin={}, pitches={}'
+        msg0=msg0.format(copy_region, 
+                         copy_src_origin, copy_src_pitches,
+                         copy_dst_origin, copy_dst_pitches)
+
+        msg0+='\n *iter arguments:'
+        msg0+='\n   region:     {}'
+        msg0+='\n   src: origin={}, pitches={}'
+        msg0+='\n   dst: origin={}, pitches={}'
+        msg0=msg0.format(iter_region, 
+                         iter_src_origin, iter_src_pitches,
+                         iter_dst_origin, iter_dst_pitches)
+        #print msg0
+        
+        return cls(varname=varname, 
+                src=src_data, dst=dst_data, 
+                copy_region=copy_region,
+                copy_src_origin=copy_src_origin,   copy_dst_origin=copy_dst_origin,
+                copy_src_pitches=copy_src_pitches, copy_dst_pitches=copy_dst_pitches,
+                iter_region=iter_region,
+                iter_src_origin=iter_src_origin,   iter_dst_origin=iter_dst_origin,
+                iter_src_pitches=iter_src_pitches, iter_dst_pitches=iter_dst_pitches)
+
+
+
+class OpenClFillKernelLauncher(OpenClCopyBufferRectLauncher):
+    """Cache buffers to perform a fill operation by using an OpenClCopyBufferRectLauncher."""
+
+    __fill_buffers = {}
+
+    @classmethod
+    def from_slices(cls, varname, backend, fill_value, dst):
+        if isinstance(dst, OpenClArray):
+            assert backend == dst.backend
+        else:
+            assert isinstance(dst, clArray.Array)
+        shape = dst.shape
+        dtype = dst.dtype
+
+        fill_value = dst.dtype.type(fill_value)
+        src = cls.create_fill_buffer(backend, dtype, shape, fill_value)
+        
+        obj = super(OpenClFillKernelLauncher, cls).from_slices(varname=varname,
+                        src=src, dst=dst)
+        return obj
+
+    @classmethod
+    def create_fill_buffer(cls, backend, dtype, shape, fill_value):
+        assert (backend.kind == Backend.OPENCL)
+        from hysop.tools.misc import prod
+        size = prod(shape)
+        key = (backend, dtype, size, fill_value)
+        if (key in cls.__fill_buffers):
+            buf = cls.__fill_buffers[key]
+        else:
+            buf = backend.full(dtype=dtype, shape=shape, 
+                    fill_value=fill_value)
+            cls.__fill_buffers[key] = buf
+        return buf.reshape(shape)
 
diff --git a/hysop/backend/device/opencl/opencl_device.py b/hysop/backend/device/opencl/opencl_device.py
index b83bd0181c3d747bdceb7cb5b55ca6cbe3e86ac3..cdd3290470a23f7d8c2ced892dd5292f92d42153 100644
--- a/hysop/backend/device/opencl/opencl_device.py
+++ b/hysop/backend/device/opencl/opencl_device.py
@@ -2,7 +2,7 @@ import re, fractions
 from hysop.tools.types import check_instance
 from hysop import vprint
 from hysop.deps import np
-from hysop.backend.device.opencl import cl
+from hysop.backend.device.opencl import cl, cl_api
 from hysop.constants import DeviceType, CacheType, MemoryType, FpConfig
 from hysop.tools.units import bytes2str, freq2str, time2str
 from hysop.backend.device.logical_device import LogicalDevice, UnknownDeviceAttribute
@@ -152,7 +152,7 @@ class OpenClDevice(LogicalDevice):
                 if isinstance(val, str):
                     val = val.strip()
                 setattr(self, '_'+attr, val)
-            except (cl.cffi_cl.LogicError, AttributeError):
+            except (cl_api.LogicError, AttributeError):
                 _not_found += (attr,)
                 setattr(self, '_'+attr, UnknownDeviceAttribute())
             except RuntimeError as e:
diff --git a/hysop/backend/device/opencl/opencl_elementwise.py b/hysop/backend/device/opencl/opencl_elementwise.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c141f1a854f1285e335a7e43de4c005194d1ee9
--- /dev/null
+++ b/hysop/backend/device/opencl/opencl_elementwise.py
@@ -0,0 +1,112 @@
+
+from hysop.constants import Precision
+from hysop.tools.types import check_instance, first_not_None, to_tuple
+from hysop.symbolic.array import OpenClSymbolicArray
+from hysop.operator.base.custom_symbolic_operator import SymbolicExpressionParser
+from hysop.backend.device.opencl.opencl_env import OpenClEnvironment
+from hysop.backend.device.opencl.opencl_kernel_config import OpenClKernelConfig
+from hysop.backend.device.opencl.opencl_symbolic import OpenClSymbolic, \
+                                                        OpenClAutotunableCustomSymbolicKernel
+
+class OpenClElementwiseKernelGenerator(object):
+
+    def __init__(self, cl_env, kernel_config=None, user_build_options=None):
+        kernel_config = first_not_None(kernel_config, OpenClKernelConfig())
+        user_build_options = to_tuple(first_not_None(user_build_options, ()))
+        check_instance(cl_env, OpenClEnvironment)
+        check_instance(kernel_config, OpenClKernelConfig)
+        check_instance(user_build_options, tuple)
+        
+        precision             = kernel_config.precision
+        if precision == Precision.SAME:
+            precision = Precision.DEFAULT
+        float_dump_mode       = kernel_config.float_dump_mode
+        use_short_circuit_ops = kernel_config.use_short_circuit_ops
+        unroll_loops          = kernel_config.unroll_loops
+        autotuner_config      = kernel_config.autotuner_config
+        typegen = cl_env.build_typegen(precision=precision,
+                                       float_dump_mode=float_dump_mode,
+                                       use_short_circuit_ops=use_short_circuit_ops,
+                                       unroll_loops=unroll_loops)
+        
+        build_options = set()
+        build_options.update(cl_env.default_build_opts)
+        build_options.update(typegen.ftype_build_options())
+        build_options.update(user_build_options)
+
+        kernel_autotuner = OpenClAutotunableCustomSymbolicKernel(
+                                cl_env=cl_env, typegen=typegen, 
+                                build_opts=tuple(build_options), 
+                                autotuner_config=autotuner_config)
+        
+        self._cl_env = cl_env
+        self._kernel_autotuner = kernel_autotuner
+    
+    
+    def elementwise_kernel(self, name, *exprs, **kwds):
+        # call_only_once means that the autotuner will stop at 
+        # first successfull kernel build and exec.
+        check_instance(name, str)
+        assert len(exprs)>0, exprs
+
+        queue = kwds.pop('queue', self._cl_env.default_queue)
+        call_only_once = kwds.pop('call_only_once', False)
+        if kwds:
+            msg='Unknown keyword arguments: {}'.format(kwds.keys())
+            raise ValueError(msg)
+        
+        expr_info = SymbolicExpressionParser.parse(name, {}, *exprs)
+        assert not expr_info.has_direction, expr_info
+
+        expr_info.compute_granularity  = 0
+        expr_info.space_discretization = None
+        expr_info.time_integrator      = None
+        expr_info.interpolation        = None
+
+        expr_info.min_ghosts                = {}
+        expr_info.min_ghosts_per_components = {}
+
+        expr_info.extract_obj_requirements()
+        expr_info.discretize_expressions(input_dfields={}, output_dfields={}, 
+                force_symbolic_axes=True)
+        expr_info.setup_expressions(None)
+        expr_info.check_arrays()
+        expr_info.check_buffers()
+        
+        kernel, args_dict, update_input_parameters = \
+                self._kernel_autotuner.autotune(expr_info=expr_info, 
+                        queue=queue, first_working=call_only_once)
+
+        kl = kernel.build_launcher(**args_dict)
+        return (kl, update_input_parameters)
+
+
+    def elementwise(self, name, *exprs, **kwds):
+        kernel, update_input_parameters = self.elementwise_kernel(name, *exprs)
+        queue = kwds.pop('queue', self._cl_env.default_queue)
+        def call_kernel(queue=queue, kernel=kernel, 
+                        update_input_parameters=update_input_parameters):
+            return kernel(queue=queue, **update_input_parameters())
+        return call_kernel
+        
+    @classmethod
+    def symbolic_buffers(cls, *names, **kwds):
+        return OpenClSymbolic.symbolic_buffers(*names, **kwds)
+
+    @classmethod
+    def symbolic_arrays(cls, *names, **kwds):
+        return OpenClSymbolic.symbolic_arrays(*names, **kwds)
+    
+    @classmethod
+    def symbolic_tmp_scalars(cls, *names, **kwds):
+        return OpenClSymbolic.symbolic_tmp_scalars(*names, **kwds)
+
+    @classmethod
+    def arrays_to_symbols(cls, *arrays):
+        symbols = ()
+        for (i,array) in enumerate(arrays):
+            name='a{}'.format(i) 
+            symbol = OpenClSymbolicArray(name=name, memory_object=array)
+            symbols += (symbol,)
+        return symbols
+
diff --git a/hysop/backend/device/opencl/opencl_env.py b/hysop/backend/device/opencl/opencl_env.py
index c6f12d930c383578c7623d05e725766845178f22..04593a335e9fa0137919efafb06ad889255641ec 100644
--- a/hysop/backend/device/opencl/opencl_env.py
+++ b/hysop/backend/device/opencl/opencl_env.py
@@ -64,7 +64,7 @@ class OpenClEnvironment(TaggedObject):
         create an OpenClEnvironment that will persist and thus maximize memory pool
         memory reuse on target device.
         """
-
+        
         super(OpenClEnvironment, self).__init__(tag_prefix='clenv', **kwds)
 
         platform_id = first_not_None(platform_id, __DEFAULT_PLATFORM_ID__)
@@ -555,11 +555,10 @@ Dumped OpenCL Kernel '{}'
         prg = cl.Program(self.context, gpu_src)
             
         dump_folder=IO.default_path()+'/'+OPENCL_KERNEL_DUMP_FOLDER
-        if not os.path.exists(dump_folder):
-            os.makedirs(dump_folder)
-        
         if DEBUG:
             # dump kernel source while in debug mode
+            if not os.path.exists(dump_folder):
+                os.makedirs(dump_folder)
             dump_file=dump_folder+'/'+'{}_dump.cl'.format(kernel_name)
             print 'Dumping kernel src at \'{}\'.'.format(dump_file)
             with open(dump_file, 'w+') as f:
@@ -575,6 +574,8 @@ Dumped OpenCL Kernel '{}'
             build = prg.build(s_build_opts)
         except Exception, e:
             # always dump source when build fails
+            if not os.path.exists(dump_folder):
+                os.makedirs(dump_folder)
             dump_file=dump_folder+'/'+'{}_build_fail.cl'.format(kernel_name)
             with open(dump_file, 'w+') as f:
                 f.write(gpu_src)
diff --git a/hysop/backend/device/opencl/opencl_fft.py b/hysop/backend/device/opencl/opencl_fft.py
index f863c0691bac538ed7b48f315ff34f6111188f28..cc4e2330a89bc24917997e325e3639f115af19d4 100644
--- a/hysop/backend/device/opencl/opencl_fft.py
+++ b/hysop/backend/device/opencl/opencl_fft.py
@@ -140,6 +140,10 @@ class OpenClFFT(FFT):
         if self._baked:
             msg='Plan was already baked.'
             raise RuntimeError(msg)
+        msg = 'Baking {}[precision={}, shape={}, inplace={}, layout_in={}, layout_out={}]'.format(
+                self.__class__.__name__, 
+                self.precision, self.t_shape, self.t_inplace, 
+                self.layout_in, self.layout_out)
         self.plan.bake(self.queue)
         self._baked = True
         return self
@@ -163,7 +167,7 @@ class OpenClFFT(FFT):
             else:
                 self.temp_buffer = buf.data
         else:
-            assert (buf is None)
+            assert (buf is None), buf
         self._allocated = True
         return self
 
@@ -295,9 +299,11 @@ class OpenClFFT(FFT):
                     msg+='\n => clFFT expected {} bytes but only {} bytes have been allocated.\n'
                     msg=msg.format(plan.required_buffer_size, tmp_buffer.nbytes)
                     raise RuntimeError(msg)
-                else:
+                elif (plan.required_buffer_size>0):
                     buf = tmp_buffer[:plan.required_buffer_size]
                     plan.allocate(buf=buf)
+                else:
+                    plan.allocate(buf=None)
         else:
             for plan in plans:
                 assert plan.required_buffer_size == 0
diff --git a/hysop/backend/device/opencl/opencl_kernel_autotuner.py b/hysop/backend/device/opencl/opencl_kernel_autotuner.py
index 648cd339400e90825e81d27f96c8fbc88fc4507e..c01af580aa541c7937b65c94940d3fe899585144 100644
--- a/hysop/backend/device/opencl/opencl_kernel_autotuner.py
+++ b/hysop/backend/device/opencl/opencl_kernel_autotuner.py
@@ -1,7 +1,7 @@
 from hysop.tools.types import check_instance
 from hysop.tools.units import bytes2str
 from hysop.tools.misc import prod
-from hysop.backend.device.opencl import cl
+from hysop.backend.device.opencl import cl, cl_api
 from hysop.backend.device.kernel_autotuner import KernelAutotuner, KernelGenerationError
 from hysop.backend.device.opencl.opencl_autotunable_kernel import OpenClAutotunableKernel
 from hysop.backend.device.opencl.opencl_kernel_statistics import OpenClKernelStatistics
@@ -50,10 +50,9 @@ class OpenClKernelAutotuner(KernelAutotuner):
 
         kwgi = cl.kernel_work_group_info
         max_kernel_wg_size = kernel.get_work_group_info(kwgi.WORK_GROUP_SIZE, self.cl_env.device)
-        kernel_compile_wg_size = kernel.get_work_group_info(kwgi.COMPILE_WORK_GROUP_SIZE, 
-                self.cl_env.device)
-        kernel_local_mem_size = kernel.get_work_group_info(kwgi.LOCAL_MEM_SIZE, 
-                self.cl_env.device)
+        preferred_work_group_size_multiple =  kernel.get_work_group_info(kwgi.PREFERRED_WORK_GROUP_SIZE_MULTIPLE,
+                    self.cl_env.device)
+        return (max_kernel_wg_size, preferred_work_group_size_multiple)
     
     def check_kernel(self, tkernel, kernel,
             global_work_size, local_work_size):
@@ -66,7 +65,7 @@ class OpenClKernelAutotuner(KernelAutotuner):
         kwgi = cl.kernel_work_group_info
         max_kernel_wg_size     = kernel.get_work_group_info(kwgi.WORK_GROUP_SIZE, device)
         kernel_local_mem_size  = kernel.get_work_group_info(kwgi.LOCAL_MEM_SIZE,  device)
-
+        
         wgs = prod(local_work_size)
 
         if (wgs > max_kernel_wg_size):
@@ -146,7 +145,7 @@ class OpenClKernelAutotuner(KernelAutotuner):
                 while(stats.nruns < target_nruns) and (not pruned):
                     try:
                         evt = cl.enqueue_nd_range_kernel(queue, kernel, global_size, local_size)
-                    except cl.cffi_cl.RuntimeError:
+                    except cl_api.RuntimeError:
                         raise KernelGenerationError()
                     evt.wait()
                     stats += OpenClKernelStatistics(events=[evt])
diff --git a/hysop/backend/device/opencl/opencl_kernel_launcher.py b/hysop/backend/device/opencl/opencl_kernel_launcher.py
index f6ba739b85b27557410cb5d5b289fe7bad567cfb..97007b619c26349a45ba6fb7da45b7e8ed3e7e02 100644
--- a/hysop/backend/device/opencl/opencl_kernel_launcher.py
+++ b/hysop/backend/device/opencl/opencl_kernel_launcher.py
@@ -298,7 +298,6 @@ class OpenClKernelLauncherI(LauncherI):
         If queue has profiling enabled, events are pushed into a local list of events
         to compute kernel statistics when self.statistics is fetched.
         """
-        pass
     
     def check_kernel_arg(self, arg, arg_id, arg_name, arg_type):
         """Check kernel argument type prior to setargs."""
@@ -329,6 +328,15 @@ class OpenClKernelLauncherI(LauncherI):
                 raise RuntimeError(msg)
 
 class HostLauncherI(LauncherI):
+
+    def __init__(self, name, **kwds):
+        super(HostLauncherI, self).__init__(name=name, **kwds)
+        self._apply_msg = '  HostLauncher.{}()'.format(name)
+
+    def __call__(self):
+        if __KERNEL_DEBUG__ or __TRACE_KERNELS__:
+            print self._apply_msg
+
     def parameters(self):
         return {}
 
diff --git a/hysop/backend/device/opencl/opencl_operator.py b/hysop/backend/device/opencl/opencl_operator.py
index 50f604cb0fac96511ff0a685f7a2b638f274095b..124c513e07e22584f1212b0d0aa18c6ca8626a83 100644
--- a/hysop/backend/device/opencl/opencl_operator.py
+++ b/hysop/backend/device/opencl/opencl_operator.py
@@ -108,8 +108,9 @@ class OpenClOperator(ComputationalGraphOperator):
             msg0='MPI Communicators do not match between OpenClEnvironment and MPIParams.'
             msg0+='\n  => {}'.format(msg)
             raise RuntimeError(msg0)
-
-    def supported_backends(self):
+    
+    @classmethod
+    def supported_backends(cls):
         """
         Return the backends that this operator's topologies can support.
         """
@@ -162,26 +163,9 @@ class OpenClOperator(ComputationalGraphOperator):
     def check(self):
         super(OpenClOperator, self).check()
         self._check_cl_env()
-
+        
     @debug
-    def get_field_requirements(self):
-        """
-        Called just after handle_method(), ie self.method has been set.
-        topology requirements are:
-            1) min and max ghosts for each input and output variables
-            2) allowed splitting directions for cartesian topologies
-            3) required local and global transposition state, if any. 
-            and more
-        they are stored in self.input_field_requirements and
-        self.output_field_requirements.
-
-        keys are continuous fields and values are of type
-        hysop.fields.field_requirement.discretefieldrequirements
-
-        default is backend.opencl, no min or max ghosts and no specific
-        transposition state for each input and output variables.
-        """
-
+    def create_topology_descriptors(self): 
         # by default we create OPENCL (gpu) TopologyDescriptors 
         for (field, topo_descriptor) in self.input_fields.iteritems():
             topo_descriptor = TopologyDescriptor.build_descriptor(
@@ -201,6 +185,24 @@ class OpenClOperator(ComputationalGraphOperator):
                     cl_env=self.cl_env)
             self.output_fields[field] = topo_descriptor
 
+    @debug
+    def get_field_requirements(self):
+        """
+        Called just after handle_method(), ie self.method has been set.
+        topology requirements are:
+            1) min and max ghosts for each input and output variables
+            2) allowed splitting directions for cartesian topologies
+            3) required local and global transposition state, if any. 
+            and more
+        they are stored in self.input_field_requirements and
+        self.output_field_requirements.
+
+        keys are continuous fields and values are of type
+        hysop.fields.field_requirement.discretefieldrequirements
+
+        default is backend.opencl, no min or max ghosts and no specific
+        transposition state for each input and output variables.
+        """
         requirements = super(OpenClOperator, self).get_field_requirements()
 
         for (is_input, reqs) in requirements.iter_requirements():
diff --git a/hysop/backend/device/opencl/opencl_symbolic.py b/hysop/backend/device/opencl/opencl_symbolic.py
index 18adca26180610d399483127802ed60dfccd6097..295ec78e2a8d76804f6a4920907e644d1514647e 100644
--- a/hysop/backend/device/opencl/opencl_symbolic.py
+++ b/hysop/backend/device/opencl/opencl_symbolic.py
@@ -27,6 +27,7 @@ from hysop.backend.device.opencl.autotunable_kernels.custom_symbolic import \
                                                 OpenClAutotunableCustomSymbolicKernel
 from hysop.tools.sympy_utils import subscript, subscripts
 
+
 class OpenClSymbolic(OpenClOperator):
     """
     Abstract class for discrete operators working on OpenCL backends 
@@ -242,7 +243,6 @@ class OpenClSymbolic(OpenClOperator):
             
             min_ghosts_per_components = {}
 
-
             for (fields, expr_info_fields, is_input, iter_requirements) in \
                     zip((self.input_fields, self.output_fields), 
                         (expr_info.input_fields, expr_info.output_fields), 
@@ -297,7 +297,7 @@ class OpenClSymbolic(OpenClOperator):
         return requirements
 
     @debug
-    def discretize(self):
+    def discretize(self, force_symbolic_axes=None):
         """
         Discretize symbolic expressions.
         """
@@ -307,11 +307,13 @@ class OpenClSymbolic(OpenClOperator):
         for expr_info in self.expr_infos.values():
             expr_info.discretize_expressions(
                 input_dfields=self.input_discrete_fields,
-                output_dfields=self.output_discrete_fields)
+                output_dfields=self.output_discrete_fields,
+                force_symbolic_axes=force_symbolic_axes)
 
     @debug
     def setup(self, work):
         for expr_info in self.expr_infos.values():
+            expr_info.setup_expressions(work)
             expr_info.check_arrays()
             expr_info.check_buffers()
         super(OpenClSymbolic, self).setup(work=work)
diff --git a/hysop/backend/device/opencl/opencl_types.py b/hysop/backend/device/opencl/opencl_types.py
index 8d5beefe2b2c0d0266a36f7f0b2a693f5b48eac4..d474928bd12fdda1aaf8a7590a0b358a3c3f01fe 100644
--- a/hysop/backend/device/opencl/opencl_types.py
+++ b/hysop/backend/device/opencl/opencl_types.py
@@ -85,6 +85,9 @@ def vtype_access(i,N,mode='hex'):
     else: return ('s' if mode.lower()=='hex' else '') + vtype_component_adressing(i,mode)
 
 def float_to_hex_str(f,fbtype):
+    if (f!=f):
+        return 'NAN'
+
     sf = float(f).hex().split('0x') + ['']
     buf = sf[1].split('p')
 
@@ -106,6 +109,8 @@ def float_to_dec_str(f,fbtype):
     """
     sf = (sign, mantissa, exponent)
     """
+    if (f!=f):
+        return 'NAN'
     sf = float(f).__repr__().split('.')
     if len(sf) == 1:
         return sf[0]
diff --git a/hysop/backend/device/opencl/operator/curl.py b/hysop/backend/device/opencl/operator/curl.py
new file mode 100644
index 0000000000000000000000000000000000000000..30d3b5afc9f4cafd9e2e8bebcd70ddf0916e4844
--- /dev/null
+++ b/hysop/backend/device/opencl/operator/curl.py
@@ -0,0 +1,79 @@
+
+import primefac, functools
+from hysop.tools.numerics import float_to_complex_dtype
+from hysop.tools.numpywrappers import npw
+from hysop.tools.decorators  import debug
+from hysop.tools.warning import HysopWarning
+from hysop.operator.base.curl import SpectralCurlOperatorBase
+from hysop.backend.device.opencl.opencl_symbolic import OpenClSymbolic
+from hysop.core.graph.graph import op_apply
+from hysop.core.memory.memory_request import MemoryRequest
+from hysop.backend.device.opencl.opencl_fft import OpenClFFT
+from hysop.backend.device.codegen.base.variables import dtype_to_ctype
+from hysop.symbolic import local_indices_symbols
+from hysop.symbolic.relational import Assignment
+from hysop.symbolic.complex import ComplexMul
+from hysop.symbolic.misc import Select
+
+class OpenClSpectralCurl(SpectralCurlOperatorBase, OpenClSymbolic):
+    """
+    Compute the curl by using an OpenCL FFT backend.
+    """
+    
+    @debug
+    def __init__(self, **kwds):
+        super(OpenClSpectralCurl, self).__init__(**kwds)
+        
+
+        assert (len(self.forward_transforms) % 2 == 0)
+        N = len(self.forward_transforms)//2
+        assert len(self.K)==2*N
+
+        kernel_names = ()
+        for (i,(Ft,(tg,Ki))) in enumerate(zip(self.forward_transforms, self.K)):
+            Fhs = Ft.output_symbolic_array('F{}_hat'.format(i))
+            
+            kname = 'filter_curl_{}d_{}'.format(Fhs.dim, i)
+            kernel_names += (kname,)
+            
+            is_complex = Ki.is_complex
+            Ki = tg._indexed_wave_numbers[Ki]
+
+            if is_complex:
+                expr = ComplexMul(Ki, Fhs)
+            else:
+                expr = Ki*Fhs
+
+            if (i<N):
+                expr = Assignment(Fhs, +expr)
+            else:
+                expr = Assignment(Fhs, -expr)
+
+            self.require_symbolic_kernel(kname, expr)
+
+        self._kernel_names = kernel_names
+    
+    @debug
+    def setup(self, work):
+        super(OpenClSpectralCurl, self).setup(work)
+        curl_filters = ()
+        for kname in self._kernel_names:
+            kernel, _ = self.symbolic_kernels[kname]
+            kernel = functools.partial(kernel, queue=self.cl_env.default_queue)
+            curl_filters += (kernel,)
+        self.curl_filters = curl_filters
+        self.exchange_ghosts = self.dFout.exchange_ghosts(build_launcher=True)
+        assert len(self.forward_transforms)==len(self.backward_transforms)==len(curl_filters)
+
+    @op_apply
+    def apply(self, **kwds):
+        """Solve the Curl equation."""
+        super(OpenClSpectralCurl, self).apply(**kwds)
+        for (Ft,filter_curl,Bt) in zip(self.forward_transforms,
+                                       self.curl_filters,
+                                       self.backward_transforms):
+            evt = Ft()
+            evt = filter_curl()
+            evt = Bt()
+        if (self.exchange_ghosts is not None):
+            evt = self.exchange_ghosts()
diff --git a/hysop/backend/device/opencl/operator/derivative.py b/hysop/backend/device/opencl/operator/derivative.py
index 703b2d52d9b98cb5697c3710fd1904ac21f2f20d..af47344331282a0173fb8cd5c2744285a96d653c 100644
--- a/hysop/backend/device/opencl/operator/derivative.py
+++ b/hysop/backend/device/opencl/operator/derivative.py
@@ -1,6 +1,7 @@
 
 from hysop.deps import sm
 from hysop.symbolic import space_symbols
+from hysop.symbolic.complex import ComplexMul
 from hysop.constants import DirectionLabels
 from hysop.backend.device.opencl.opencl_array_backend import OpenClArrayBackend
 from hysop.tools.decorators import debug
@@ -13,15 +14,17 @@ from hysop.backend.device.opencl.autotunable_kernels.custom_symbolic import Open
 from hysop.backend.device.opencl.opencl_kernel_launcher import OpenClKernelListLauncher
 from hysop.backend.device.opencl.opencl_copy_kernel_launchers import OpenClCopyBufferRectLauncher
 
-from hysop.operator.base.derivative import SpaceDerivativeBase
+from hysop.operator.base.derivative import FiniteDifferencesSpaceDerivativeBase, \
+                                           SpectralSpaceDerivativeBase
 from hysop.operator.base.custom_symbolic_operator import SymbolicExpressionParser
 from hysop.symbolic.relational import Assignment
 
-class OpenClSpaceDerivative(SpaceDerivativeBase, OpenClSymbolic):
 
+class OpenClFiniteDifferencesSpaceDerivative(FiniteDifferencesSpaceDerivativeBase, 
+                                             OpenClSymbolic):
     @debug
     def __init__(self, **kwds):
-        super(OpenClSpaceDerivative, self).__init__(require_tmp=False, **kwds)
+        super(OpenClFiniteDifferencesSpaceDerivative, self).__init__(require_tmp=False, **kwds)
         
         Fin  = self.Fin.s()
         Fout = self.Fout.s()
@@ -30,16 +33,16 @@ class OpenClSpaceDerivative(SpaceDerivativeBase, OpenClSymbolic):
             A = A.s()
         elif self.scale_by_parameter:
             if (A.size > 1):
-                A = A.s
+                raise NotImplementedError
             else:
                 A = A.s
         xd = space_symbols[self.direction]
-        expr = Assignment(Fout, A*Fin.diff(xd, self.derivative))
+        expr = Assignment(Fout, A*Fin.diff(xd, self.directional_derivative))
         self.require_symbolic_kernel('compute_derivative', expr)
     
     @debug
     def setup(self, work):
-        super(OpenClSpaceDerivative, self).setup(work)
+        super(OpenClFiniteDifferencesSpaceDerivative, self).setup(work)
         (self.kernel, self.update_parameters) = self.symbolic_kernels['compute_derivative']
     
     @op_apply
@@ -47,3 +50,92 @@ class OpenClSpaceDerivative(SpaceDerivativeBase, OpenClSymbolic):
         queue = self.cl_env.default_queue
         evt = self.kernel(queue=queue, **self.update_parameters())
         evt = self.dFout.exchange_ghosts(queue=queue, evt=evt)
+
+
+class OpenClSpectralSpaceDerivative(SpectralSpaceDerivativeBase, OpenClSymbolic):
+    """
+    Compute a derivative of a scalar field in a given direction
+    using spectral methods.
+    """
+    
+    @debug
+    def __init__(self, **kwds):
+        """
+        Initialize a SpectralSpaceDerivative operator on the opencl backend.
+
+        See hysop.operator.base.derivative.SpectralSpaceDerivativeBase for 
+        more information.
+
+        Parameters
+        ----------
+        kwds: dict, optional
+            Base class arguments.
+        """
+        super(OpenClSpectralSpaceDerivative, self).__init__(**kwds)
+        Fs  = self.Fin.s()
+        dFs = self.Fout.s()
+        Fhs = self.Ft.output_symbolic_array('Fhat')
+        if self.scale_by_field:
+            As = A.s()
+        elif self.scale_by_parameter:
+            if (A.size > 1):
+                raise NotImplementedError
+            else:
+                As = A.s
+        elif self.scale_by_value:
+            As = A
+        else:
+            As = None
+        if (As is not None):
+            assert (self.scale_by_value or self.scale_by_parameter or self.scale_by_field)
+            expr = Assignment(Fout, As*Fout)
+            self.require_symbolic_kernel('scale_derivative', expr)
+            self._do_scale = True
+        else:
+            self._do_scale = False
+        
+        Kr = 1
+        Kc = None
+        for (wn, indexed_wn) in self.tg._indexed_wave_numbers.iteritems():
+            if wn.is_real:
+                Kr *= indexed_wn
+            else:
+                assert wn.is_complex
+                if (Kc is None):
+                    Kc = indexed_wn
+                else:
+                    Kc = ComplexMul(Kc, indexed_wn)
+        if (Kc is None):
+            rhs = Kr*Fhs
+        else:
+            rhs = Kr*ComplexMul(Kc, Fhs)
+
+        expr = Assignment(Fhs, rhs)
+
+        self.require_symbolic_kernel('compute_derivative', expr)
+        self.Fhs = Fhs
+
+    @debug
+    def discretize(self, **kwds):
+        super(OpenClSpectralSpaceDerivative, self).discretize(**kwds)
+    
+    @debug
+    def setup(self, work):
+        super(OpenClSpectralSpaceDerivative, self).setup(work=work)
+        if self._do_scale:
+            (self.scale_kernel, self.scale_update_parameters) = self.symbolic_kernels['scale_derivative']
+        else:
+            self.scale_derivative_kernel = lambda **kwds: None
+            self.scale_update_parameters = lambda: {} 
+        self.compute_derivative_kernel, _ = self.symbolic_kernels['compute_derivative']
+
+    
+    @op_apply
+    def apply(self, **kwds):
+        queue = self.cl_env.default_queue
+        self.Ft().wait()
+        self.compute_derivative_kernel(queue=queue).wait()
+        self.Bt().wait()
+        self.scale_derivative_kernel(queue=queue, **self.scale_update_parameters())
+        self.dFout.exchange_ghosts()
+
diff --git a/hysop/backend/device/opencl/operator/diffusion.py b/hysop/backend/device/opencl/operator/diffusion.py
new file mode 100644
index 0000000000000000000000000000000000000000..50ec9dd4a07a5ad36342e926fd9b085d03165f0e
--- /dev/null
+++ b/hysop/backend/device/opencl/operator/diffusion.py
@@ -0,0 +1,77 @@
+
+import primefac, functools
+from hysop.tools.numerics import float_to_complex_dtype
+from hysop.tools.numpywrappers import npw
+from hysop.tools.decorators  import debug
+from hysop.tools.warning import HysopWarning
+from hysop.operator.base.diffusion import DiffusionOperatorBase
+from hysop.backend.device.opencl.opencl_symbolic import OpenClSymbolic
+from hysop.core.graph.graph import op_apply
+from hysop.core.memory.memory_request import MemoryRequest
+from hysop.backend.device.opencl.opencl_fft import OpenClFFT
+from hysop.backend.device.codegen.base.variables import dtype_to_ctype
+from hysop.symbolic import local_indices_symbols
+from hysop.symbolic.relational import LogicalAND, LogicalEQ, Assignment
+
+class OpenClDiffusion(DiffusionOperatorBase, OpenClSymbolic):
+    """
+    Solves the diffusion equation using an OpenCL FFT backend.
+    """
+    
+    @debug
+    def __init__(self, **kwds):
+        super(OpenClDiffusion, self).__init__(**kwds)
+        queue = self.cl_env.default_queue
+
+        nu, dt = self.nu.s, self.dt.s
+
+        kernel_names = ()
+        for (i,(Ft,Wn)) in enumerate(zip(self.forward_transforms, self.wave_numbers)):
+            Fhs = Ft.output_symbolic_array('F{}_hat'.format(i))
+            indices = local_indices_symbols[:Fhs.dim]
+        
+            kname = 'filter_diffusion_{}d_{}'.format(Fhs.dim, i)
+            kernel_names += (kname,)
+
+            F = 0
+            for Wi in Wn:
+                indexed_Wi = self.tg._indexed_wave_numbers[Wi]
+                F += indexed_Wi
+            expr = Assignment(Fhs, Fhs / (1 - nu*dt*F))
+            
+            self.require_symbolic_kernel(kname, expr)
+
+        self._kernel_names = kernel_names
+    
+    @debug
+    def setup(self, work):
+        super(OpenClDiffusion, self).setup(work)
+        queue = self.cl_env.default_queue
+
+        diffusion_filters = ()
+        for kname in self._kernel_names:
+            kernel, up = self.symbolic_kernels[kname]
+            kernel = lambda kernel=kernel, queue=queue, up=up: kernel(queue=queue, **up())
+            diffusion_filters += (kernel,)
+
+        eg = self.dFout.exchange_ghosts(build_launcher=True)
+        if (eg is not None):
+            eg = functools.partial(eg, queue=queue)
+
+        self._diffusion_filters = diffusion_filters
+        self._exchange_ghosts = eg
+
+    @op_apply
+    def apply(self, **kwds):
+        """Solve the Diffusion equation."""
+        super(OpenClDiffusion, self).apply(**kwds)
+        exchange_ghosts = self._exchange_ghosts
+        for (Ft,Bt,filter_diffusion) in zip(
+                self.forward_transforms,
+                self.backward_transforms,
+                self._diffusion_filters):
+            evt = Ft()
+            evt = filter_diffusion()
+            evt = Bt()
+        if exchange_ghosts:
+            evt = exchange_ghosts()
diff --git a/hysop/backend/device/opencl/operator/directional/advection_dir.py b/hysop/backend/device/opencl/operator/directional/advection_dir.py
index 91e3a919c466b2bc3836c1024aa7a7fede901f6a..c7e3ff9c411f4f658cb3742961e3802412923e1e 100644
--- a/hysop/backend/device/opencl/operator/directional/advection_dir.py
+++ b/hysop/backend/device/opencl/operator/directional/advection_dir.py
@@ -79,7 +79,7 @@ class OpenClDirectionalAdvection(DirectionalAdvectionBase, OpenClDirectionalOper
     def _collect_kernels(self):
         kl  = OpenClKernelListLauncher(name='advec_remesh')
         kl += self._collect_advection_kernel()
-        kl += self._collect_remesh_kernel()
+        kl += self._collect_remesh_kernels()
         kl += self._collect_redistribute_kernels()
         self.all_kernels = kl
 
@@ -116,7 +116,7 @@ class OpenClDirectionalAdvection(DirectionalAdvectionBase, OpenClDirectionalOper
         self.advection_kernel_launcher = advec_launcher
         return advec_launcher
 
-    def _collect_remesh_kernel(self):
+    def _collect_remesh_kernels(self):
         cl_env           = self.cl_env
         typegen          = self.typegen
         build_options    = self.build_options()
@@ -136,20 +136,21 @@ class OpenClDirectionalAdvection(DirectionalAdvectionBase, OpenClDirectionalOper
         kwds['is_inplace'] = self.is_inplace
 
         kwds['position']    = self.dposition
-        kwds['scalars_in']  = scalars_in
-        kwds['scalars_out'] = scalars_out
         kwds['is_inplace']  = self.is_inplace
 
         kwds['remesh_kernel']       = self.remesh_kernel
         kwds['remesh_criteria_eps'] = self.remesh_criteria_eps
         kwds['force_atomics']       = self.force_atomics
         kwds['relax_min_particles'] = self.relax_min_particles
-
-        (remesh_kernel, args_dict) = kernel.autotune(force_verbose=self._force_autotuner_verbose,
-                force_debug=self._force_autotuner_debug, hardcode_arrays=True, **kwds)
-
-        kl = remesh_kernel.build_launcher(**args_dict)
-        self.remesh_kernel_launcher = kl
+        
+        assert len(scalars_in)==len(scalars_out)
+        kl = OpenClKernelListLauncher(name='remesh')
+        for (Sin, Sout) in zip(scalars_in, scalars_out):
+            kwds['scalars_in']  = (Sin,)
+            kwds['scalars_out'] = (Sout,)
+            (remesh_kernel, args_dict) = kernel.autotune(force_verbose=self._force_autotuner_verbose,
+                    force_debug=self._force_autotuner_debug, hardcode_arrays=True, **kwds)
+            kl += remesh_kernel.build_launcher(**args_dict)
         return kl
 
     def _collect_redistribute_kernels(self):
@@ -193,7 +194,7 @@ class OpenClDirectionalAdvection(DirectionalAdvectionBase, OpenClDirectionalOper
             print 'OPENCL_Sout (after accumulation, no ghosts)'
             self.dadvected_fields_out.values()[0].print_with_ghosts()
         else:
-            self.all_kernels(queue=queue, dt=dt).wait()
+            self.all_kernels(queue=queue, dt=dt)
 
     @classmethod
     def supports_mpi(cls):
diff --git a/hysop/backend/device/opencl/operator/external_force.py b/hysop/backend/device/opencl/operator/external_force.py
new file mode 100644
index 0000000000000000000000000000000000000000..7fe49b28518f6d17546c6f38805f896a7060cb1e
--- /dev/null
+++ b/hysop/backend/device/opencl/operator/external_force.py
@@ -0,0 +1,353 @@
+
+import primefac, functools
+import sympy as sm
+
+from hysop.tools.numerics import float_to_complex_dtype
+from hysop.tools.numpywrappers import npw
+from hysop.tools.decorators  import debug
+from hysop.tools.warning import HysopWarning
+from hysop.tools.types import first_not_None, to_tuple, check_instance
+from hysop.core.graph.graph import op_apply
+from hysop.core.memory.memory_request import MemoryRequest
+from hysop.fields.continuous_field import Field, ScalarField
+from hysop.parameters.tensor_parameter import TensorParameter
+from hysop.parameters.scalar_parameter import ScalarParameter
+from hysop.backend.device.opencl.opencl_fft import OpenClFFT
+from hysop.backend.device.opencl.opencl_symbolic import OpenClSymbolic
+from hysop.backend.device.codegen.base.variables import dtype_to_ctype
+from hysop.symbolic import local_indices_symbols
+from hysop.symbolic.relational import Assignment
+from hysop.symbolic.complex import ComplexMul
+from hysop.symbolic.misc import Select
+from hysop.symbolic.field import AppliedSymbolicField, SymbolicField, curl, laplacian
+from hysop.symbolic.parameter import SymbolicScalarParameter
+from hysop.symbolic.spectral import AppliedSpectralTransform
+from hysop.operator.base.external_force import ExternalForce, SpectralExternalForceOperatorBase
+
+class OpenClSpectralExternalForce(SpectralExternalForceOperatorBase, OpenClSymbolic):
+    """
+    Operator to compute the curl of a symbolic expression.
+    """
+    
+    @debug
+    def __init__(self, Fext, **kwds):
+        check_instance(Fext, SymbolicExternalForce)
+        super(OpenClSpectralExternalForce, self).__init__(Fext=Fext, **kwds)
+
+
+
+class SymbolicExternalForce(ExternalForce):
+    def __init__(self, name, Fext, diffusion=None, **kwds):
+        """
+        Specify an external force as a tuple of symbolic expressions.
+
+        2D ExternalForce example:  
+            1) Fext = -rho*g*e_y where rho is a field and g a constant
+                Fext = (0, -rho.s()*g)
+            2) Fext = (Rs*S+C)*e_y
+                Fext = (0, -Rs*S.s()+C.s())
+        """
+        Fext = to_tuple(Fext)
+        dim = len(Fext)
+
+        Fext = list(Fext)
+        for i,e in enumerate(Fext):
+            if isinstance(e, type(None)):
+                Fext[i] = 0 # curl(const) = 0
+            msg='Expression "{}" contains a SymbolicField, did you forget to apply it ?'
+            msg=msg.format(e)
+            if isinstance(e, sm.Basic):
+                assert not e.atoms(SymbolicField), msg
+        Fext = tuple(Fext)
+
+        super(SymbolicExternalForce, self).__init__(name=name, dim=dim, Fext=Fext, **kwds)
+        
+        diffusion = first_not_None(diffusion, {})
+        diffusion = {k:v for (k,v) in diffusion.iteritems() if (v is not None)}
+        for (k,v) in diffusion.iteritems():
+            assert k in self.input_fields(), k.short_description()
+            assert isinstance(v, ScalarParameter)
+        self._diffusion = diffusion
+
+    def input_fields(self):
+        return set(map(lambda f: f.field, self._extract_objects(AppliedSymbolicField)))
+    def output_fields(self):
+        return set(self._diffusion.keys())
+    def input_params(self):
+        p0 = set(map(lambda p: p.parameter, self._extract_objects(SymbolicScalarParameter)))
+        p1 = set(self._diffusion.values())
+        return p0.union(p1)
+    def output_params(self):
+        return set()
+    
+    def initialize(self, op):
+        tg = op.new_transform_group()
+        fft_fields = tuple(self.input_fields())
+        forward_transforms  = {}
+        backward_transforms = {}
+        for Si in fft_fields:
+            Fi = tg.require_forward_transform(Si)
+            forward_transforms[Si]  = Fi
+            if (Si in self.diffusion):
+                Bi = tg.require_backward_transform(Si)
+                backward_transforms[Si] = Bi
+
+        force_backward_transforms = {}
+        for Fi in op.force.fields:
+            force_backward_transforms[Fi] = tg.require_backward_transform(Fi,
+                    custom_input_buffer='B0')
+
+        frame = None
+        fft_expressions = ()
+        for e in self.Fext:
+            if isinstance(e, sm.Basic):
+                efields = tuple(e.atoms(AppliedSymbolicField))
+                for sf in efields:
+                    field = sf.field
+                    assert field in forward_transforms, field.name
+                    if field in self._diffusion:
+                        assert field in backward_transforms, field.name
+                replace = {sf:forward_transforms[sf.field].s for sf in efields}
+                frame = replace.values()[0].frame
+                e = e.xreplace(replace)
+            fft_expressions += (e,)
+        
+        if (frame is None):
+            msg='Could not extract frame from expressions.'
+            raise RuntimeError(frame)
+        fft_expressions = to_tuple(curl(fft_expressions, frame))
+        
+        self.tg = tg
+        self.forward_transforms  = forward_transforms
+        self.backward_transforms = backward_transforms
+        self.force_backward_transforms = force_backward_transforms
+        self.fft_expressions = fft_expressions
+        self.compute_required_kernels(op)
+
+    def compute_required_kernels(self, op):
+        dts  = op.dt.s
+        forces = op.force.s()
+        diffusion_kernels = {}
+        for (f, nu) in self.diffusion.iteritems():
+            nus = nu.s
+            kname = 'filter_diffusion_{}d_{}'.format(f.dim, f.name)
+            Ft = self.forward_transforms[f]
+            Fs = Ft.output_symbolic_array('{}_hat'.format(f.name))
+            E = 0
+            Wn = self.tg.push_expressions(laplacian(Ft.s, Ft.s.frame))
+            for Wi in Wn:
+                Wi = self.tg._indexed_wave_numbers[Wi]
+                E += Wi
+            expr = Assignment(Fs, Fs / (1 - nus*dts*E))
+            op.require_symbolic_kernel(kname, expr)
+            diffusion_kernels[f] = kname
+        
+        force_kernels = ()
+        vorticity_kernels = ()
+        assert len(op.vorticity.fields)==len(op.force.fields)==len(self.fft_expressions)
+        for (Fi,Wi,e) in zip(
+                op.force.fields,
+                op.vorticity.fields, 
+                self.fft_expressions):
+            if (e==0):
+                force_kernels     += (None,)
+                vorticity_kernels += (None,)
+                continue
+            
+            Fi_hat = self.force_backward_transforms[Fi]
+            Fi_buf = Fi_hat.input_symbolic_array('{}_hat'.format(Fi.name))
+            Wn     = self.tg.push_expressions(Assignment(Fi_hat, e))
+            
+            msg='Could not extract transforms.'
+            try:
+                transforms = e.atoms(AppliedSpectralTransform)
+            except AttributeError:
+                raise RuntimeError(msg)
+            assert len(transforms)>=1, msg
+
+            fft_buffers = { Ft.s: Ft.output_symbolic_array('{}_hat'.format(Ft.field.name)) 
+                                for Ft in self.forward_transforms.values() }
+            wavenumbers = { Wi: self.tg._indexed_wave_numbers[Wi] 
+                                for Wi in Wn }
+
+            replace = {}
+            replace.update(fft_buffers)
+            replace.update(wavenumbers)
+            expr = e.xreplace(replace)
+            expr = Assignment(Fi_buf, expr)
+            
+            kname = 'compute_{}'.format(Fi.var_name)
+            op.require_symbolic_kernel(kname, expr)
+            force_kernels += (kname,)
+
+            Fis = Fi.s()
+            Wis = Wi.s()
+            expr = Assignment(Wis, Wis + dts*Fis)
+            kname = 'update_{}'.format(Wi.var_name)
+            op.require_symbolic_kernel(kname, expr)
+            vorticity_kernels += (kname,)
+
+        assert len(diffusion_kernels) == len(self.diffusion)
+        assert len(force_kernels) == op.vorticity.nb_components == len(vorticity_kernels)
+        self.diffusion_kernel_names = diffusion_kernels
+        self.force_kernel_names     = force_kernels
+        self.vorticity_kernel_names = vorticity_kernels
+
+    def discretize(self, op):
+        pass
+    
+    def get_mem_requests(self, op):
+        requests = {}
+        for Fi in self.forward_transforms.keys(): 
+            Ft = self.forward_transforms[Fi]
+            Bt = self.backward_transforms.get(Fi, None)
+            if (Bt is not None):
+                assert (Ft.backend is Bt.backend)
+                assert (Ft.output_dtype == Bt.input_dtype), (Ft.output_dtype, Bt.input_dtype)
+                assert (Ft.output_shape == Bt.input_shape), (Ft.output_shape, Bt.input_shape)
+            shape = Ft.output_shape
+            dtype = Ft.output_dtype
+            request = MemoryRequest(backend=self.tg.backend, dtype=dtype, 
+                                    shape=shape, nb_components=1,
+                                    alignment=op.min_fft_alignment)
+            name = '{}_hat'.format(Ft.field.name)
+            requests[name] = request
+        return requests
+    
+    def pre_setup(self, op, work):
+        for Fi in self.forward_transforms.keys(): 
+            Ft = self.forward_transforms[Fi]
+            Bt = self.backward_transforms.get(Fi, None)
+            dtmp, = work.get_buffer(op, '{}_hat'.format(Ft.field.name))
+            Ft.configure_output_buffer(dtmp)
+            if (Bt is not None):
+                Bt.configure_input_buffer(dtmp)
+
+    def post_setup(self, op, work):
+        diffusion_kernels  = {}
+        force_kernels      = {}
+        compute_statistics = {}
+        vorticity_kernels  = {}
+        ghost_exchangers   = {}
+
+        queue = self.tg.backend.cl_env.default_queue
+        def build_launcher(knl, update_params):
+            def kernel_launcher(knl=knl, update_params=update_params, queue=queue):
+                kwds = update_params()
+                return knl(queue=queue, **kwds)
+            return kernel_launcher
+        
+        for (field, kname) in self.diffusion_kernel_names.iteritems():
+            dfield = op.get_input_discrete_field(field)
+            knl, update_params = op.symbolic_kernels[kname]
+            diffusion_kernels[field] = build_launcher(knl, update_params)
+            ghost_exchangers[field] = functools.partial(dfield.build_ghost_exchanger(),
+                                                                            queue=queue)
+        
+        if (op.Fmin is not None):
+            min_values = npw.asarray(op.Fmin()).copy()
+        if (op.Fmax is not None):
+            max_values = npw.asarray(op.Fmax()).copy()
+
+        for i, (kname0, kname1) in enumerate(zip(
+            self.force_kernel_names, self.vorticity_kernel_names)):
+            if (kname0 is None):
+                assert (kname1 is None)
+                continue
+            Wi  = op.vorticity.fields[i]
+            Fi  = op.force.fields[i]
+            dWi = op.dW.dfields[i]
+            dFi = op.dF.dfields[i]
+            
+            knl, update_params = op.symbolic_kernels[kname0]
+            force_kernels[(Fi,Wi)]  = build_launcher(knl, update_params)
+            
+            knl, update_params = op.symbolic_kernels[kname1]
+            vorticity_kernels[(Fi,Wi)] = build_launcher(knl, update_params)
+
+            ghost_exchangers[Wi] = functools.partial(dWi.build_ghost_exchanger(), queue=queue)
+            
+            def compute_statistic(op=op, queue=queue, dFi=dFi, 
+                                min_values=min_values, max_values=max_values):
+                if (op.Fmin is not None):
+                    min_values[i] = dFi.sdata.min(queue=queue).get()
+                if (op.Fmax is not None):
+                    max_values[i] = dFi.sdata.max(queue=queue).get()
+            compute_statistics[Fi] = compute_statistic
+        
+        def update_statistics(op=op, min_values=min_values, max_values=max_values):
+            if (op.Fmin is not None):
+                op.Fmin.value = min_values
+            if (op.Fmax is not None):
+                op.Fmax.value = max_values
+            if (op.Finf is not None):
+                op.Finf.value = npw.maximum(npw.abs(min_values), npw.abs(max_values))
+                
+        assert len(diffusion_kernels) == len(self.diffusion) == len(self.backward_transforms)
+        assert len(vorticity_kernels) == len(force_kernels) == len(self.force_backward_transforms)
+        assert len(ghost_exchangers) == len(diffusion_kernels) + len(vorticity_kernels)
+
+        self.diffusion_kernels  = diffusion_kernels
+        self.force_kernels      = force_kernels
+        self.vorticity_kernels  = vorticity_kernels
+        self.ghost_exchangers   = ghost_exchangers
+        self.compute_statistics = compute_statistics
+        self.update_statistics  = update_statistics
+
+    def apply(self, op, **kwds):
+        for (field, Ft) in self.forward_transforms.iteritems():
+            evt = Ft()
+            if (field in self.backward_transforms):
+                evt = self.diffusion_kernels[field]()
+                evt = self.backward_transforms[field]()
+                evt = self.ghost_exchangers[field]()
+        
+        for (Fi,Wi) in self.force_kernels.keys():
+            evt = self.force_kernels[(Fi,Wi)]()
+            evt = self.force_backward_transforms[Fi]()
+            evt = self.compute_statistics[Fi]()
+            evt = self.vorticity_kernels[(Fi,Wi)]()
+            evt = self.ghost_exchangers[Wi]()
+
+        self.update_statistics()
+
+    def _extract_objects(self, obj_type):
+        objs = set()
+        for e in self.Fext:
+            try:
+                objs.update(e.atoms(obj_type))
+            except AttributeError:
+                pass
+        return objs
+
+    def short_description(self):
+        return 'SymbolicExternalForce[name={}]'.format(self.name)
+    
+    def long_description(self):
+        sep = '\n      *'
+        expressions = sep + sep.join('F{} = {}'.format(x,e) for (x,e) in zip('xyz',self.Fext))
+        diffusion = sep + sep.join('{}: {}'.format(f.pretty_name, p.pretty_name) 
+                                        for (f,p) in self.diffusion.iteritems())
+        input_fields  = ', '.join(f.pretty_name for f in self.input_fields())
+        output_fields = ', '.join(f.pretty_name for f in self.output_fields())
+        input_params  = ', '.join(p.pretty_name for p in self.input_params())
+        output_params = ', '.join(p.pretty_name for p in self.output_params())
+        
+        ss = \
+        '''SymbolicExternalForce:
+    name:          {}
+    pretty_name:   {}
+    expressions:   {}
+    diffusion:     {}
+    -----------------
+    input_fields:  {}
+    output_fields: {}
+    input_params:  {}
+    output_params: {}
+        '''.format(self.name, self.pretty_name, 
+                expressions, diffusion, 
+                input_fields, output_fields,
+                input_params, output_params)
+        return ss
+
+
diff --git a/hysop/backend/device/opencl/operator/min_max.py b/hysop/backend/device/opencl/operator/min_max.py
index 6a3c74f3aec29baee4f47ab2cbd35b03be8de50f..03a88e9e53e1c0eed3055a416b218df0dcd75292 100644
--- a/hysop/backend/device/opencl/operator/min_max.py
+++ b/hysop/backend/device/opencl/operator/min_max.py
@@ -4,17 +4,13 @@ from hysop.core.graph.graph import op_apply
 from hysop.operator.base.min_max import MinMaxFieldStatisticsBase, \
                                         MinMaxDerivativeStatisticsBase
 from hysop.backend.device.opencl.opencl_operator import OpenClOperator
-from hysop.backend.device.opencl.operator.derivative import OpenClSpaceDerivative
+from hysop.backend.device.opencl.operator.derivative import OpenClSpectralSpaceDerivative, \
+                                                            OpenClFiniteDifferencesSpaceDerivative
 
 
 class OpenClMinMaxFieldStatistics(MinMaxFieldStatisticsBase, OpenClOperator):
     """OpenCl implementation backend of operator MinMaxFieldStatistics."""
 
-    @debug
-    def __init__(self, **kwds):
-        """See MinMaxFieldStatisticsBase.__init__()."""
-        super(OpenClMinMaxFieldStatistics, self).__init__(**kwds)
-
     @op_apply
     def apply(self, **kwds):
         """See MinMaxFieldStatisticsBase.compute_statistics()."""
@@ -22,17 +18,21 @@ class OpenClMinMaxFieldStatistics(MinMaxFieldStatisticsBase, OpenClOperator):
         self.compute_statistics(**kwds)
 
 
-class OpenClMinMaxDerivativeStatistics(MinMaxDerivativeStatisticsBase, OpenClSpaceDerivative):
-    """OpenCl implementation backend of operator MinMaxDerivativeStatistics."""
+class OpenClMinMaxSpectralDerivativeStatistics(MinMaxDerivativeStatisticsBase, 
+                                               OpenClSpectralSpaceDerivative):
+    """OpenCl implementation backend of operator MinMaxSpectralDerivativeStatistics."""
+    @op_apply
+    def apply(self, **kwds):
+        """Compute derivative and than statistics."""
+        super(OpenClMinMaxSpectralDerivativeStatistics, self).apply(**kwds)
+        self.compute_statistics(**kwds)
 
-    @debug
-    def __init__(self, **kwds):
-        """See MinMaxDerivativeStatisticsBase.__init__()."""
-        super(OpenClMinMaxDerivativeStatistics, self).__init__(**kwds)
 
+class OpenClMinMaxFiniteDifferencesDerivativeStatistics(MinMaxDerivativeStatisticsBase, 
+                                                        OpenClFiniteDifferencesSpaceDerivative):
+    """OpenCl implementation backend of operator MinMaxFiniteDifferencesDerivativeStatistics."""
     @op_apply
     def apply(self, **kwds):
         """Compute derivative and than statistics."""
-        super(OpenClMinMaxDerivativeStatistics, self).apply(**kwds)
+        super(OpenClMinMaxFiniteDifferencesDerivativeStatistics, self).apply(**kwds)
         self.compute_statistics(**kwds)
-
diff --git a/hysop/backend/device/opencl/operator/poisson.py b/hysop/backend/device/opencl/operator/poisson.py
index acce985d72d420d90f83cdc7e645765b2d833a2c..27690da7e7b30268a46d70c12d8f7a53f684e838 100644
--- a/hysop/backend/device/opencl/operator/poisson.py
+++ b/hysop/backend/device/opencl/operator/poisson.py
@@ -1,5 +1,5 @@
 
-import primefac
+import primefac, functools
 from hysop.tools.numerics import float_to_complex_dtype
 from hysop.tools.numpywrappers import npw
 from hysop.tools.decorators  import debug
@@ -10,167 +10,60 @@ from hysop.core.graph.graph import op_apply
 from hysop.core.memory.memory_request import MemoryRequest
 from hysop.backend.device.opencl.opencl_fft import OpenClFFT
 from hysop.backend.device.codegen.base.variables import dtype_to_ctype
+from hysop.symbolic import local_indices_symbols
+from hysop.symbolic.relational import LogicalAND, LogicalEQ, Assignment
+from hysop.symbolic.misc import Select
 
 class OpenClPoisson(PoissonOperatorBase, OpenClSymbolic):
     """
-    Solves the poisson equation using clFFT.
+    Solves the poisson equation using an OpenCL FFT backend.
     """
     
-    def generate_wave_numbers(self, dim, resolution, length, dtype, axes):
-        if (dim>3):
-            msg='clFFT only support 1D, 2D or 3D plans, got a {}D domain.'
-            msg=msg.format(dim)
-            raise ValueError(msg)
-
-        valid_factors = {2,3,5,7,11,13}
-        for Ni in resolution:
-            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(resolution, Ni, factorization, candidates)
-                raise ValueError(msg)
-
-        K = ()
-        for i in axes:
-            Ni = resolution[i]
-            Li = length[i]
-
-            if (i==dim-1):
-                k = 2*npw.pi*1j*npw.fft.rfftfreq(Ni,Li)*Ni
-                k = (k**2).real.astype(dtype=dtype).copy()
-            else:
-                k = 2*npw.pi*1j*npw.fft.fftfreq(Ni, Li)*Ni
-                k = (k**2).real.astype(dtype=dtype).copy()
-            K += (k,)
-        
-        Ksize = sum(k.size for k in K)
-        Kd = self.backend.empty(shape=(Ksize,), dtype=dtype)
-        start,end = 0,0
-        Koffsets = ()
-        Ksizes = ()
-        for k in K:
-            Koffsets += (start,)
-            Ksizes   += (k.size,)
-            end += k.size
-            Kd[start:end] = k
-            start = end
-        
-        self.Kd         = Kd
-        self.Kd_sizes   = Ksizes
-        self.Kd_offsets = Koffsets
-    
     @debug
-    def get_work_properties(self):
-        requests  = super(OpenClPoisson,self).get_work_properties()
+    def __init__(self, **kwds):
+        super(OpenClPoisson, self).__init__(**kwds)
         
-        axes  = self.axes
-        shape = list(self.resolution)
-        shape[axes[0]] = shape[axes[0]] // 2 + 1
-        assert npw.array_equal(shape, self.Kd_sizes[::-1])
+        kernel_names = ()
+        for (i,(Ft,Wn)) in enumerate(zip(self.forward_transforms, self.wave_numbers)):
+            Fhs = Ft.output_symbolic_array('F{}_hat'.format(i))
+            indices = local_indices_symbols[:Fhs.dim]
+            
+            kname = 'filter_poisson_{}d_{}'.format(Fhs.dim, i)
+            kernel_names += (kname,)
 
-        dtype = float_to_complex_dtype(self.dtype)
-        request = MemoryRequest.empty_like(a=self.dFin, shape=shape, dtype=dtype, 
-                nb_components=1)
-        requests.push_mem_request('R2C_C2R', request)
-        
-        return requests
+            F = 0
+            for Wi in Wn:
+                indexed_Wi = self.tg._indexed_wave_numbers[Wi]
+                F += indexed_Wi
+            cond = LogicalAND(*tuple(LogicalEQ(idx,0) for idx in indices))
+            expr = Assignment(Fhs, Select(Fhs/F, 0, cond))
+            
+            self.require_symbolic_kernel(kname, expr)
 
+        self._kernel_names = kernel_names
+    
     @debug
     def setup(self, work):
         super(OpenClPoisson, self).setup(work)
-        self._build_fft_plans(work)
-        self._build_ghost_exchanger()
-
-    def _build_fft_plans(self, work):
-        axes    = self.axes
-        context = self.backend.cl_env.context
-        queue   = self.backend.cl_env.default_queue
-        
-        fft_buffer = work.get_buffer(self, 'R2C_C2R')[0]
-        forward_plans, backward_plans = [],[]
-
-        for (ib,ob) in zip(self.in_buffers, self.out_buffers):
-            fp = OpenClFFT(context=context, queue=queue, 
-                        in_array=ib, out_array=fft_buffer.handle,
-                        axes=axes, fast_math=False,
-                        real=True)
-            (forward_callbacks, user_data) = self._build_callbacks(fp)
-            fp.plan.set_callback('post_callback', forward_callbacks['post'],
-                                    'post', user_data=user_data)
-            fp.bake()
-            forward_plans.append(fp)
-
-            bp = OpenClFFT(context=context, queue=queue, 
-                        in_array=fft_buffer.handle, out_array=ob,
-                        axes=axes, fast_math=False,
-                        real=True)
-            bp.bake()
-            backward_plans.append(bp)
-        
-        all_plans = forward_plans + backward_plans
-        tmp_buffer = OpenClFFT.allocate_plans(self, all_plans)
-
-        self.forward_plans  = forward_plans
-        self.backward_plans = backward_plans
-        self.fft_buffer = fft_buffer
-        self.tmp_buffer = tmp_buffer
-        self.queue = queue
-
-    def _build_callbacks(self, plan):
-        fp = dtype_to_ctype(self.dtype)
-
-        (offsets, sizes) = (self.Kd_offsets, self.Kd_sizes)
-        gencode = ''.join(
-                  '''
-                                i = (off % {Si});
-                                off /= {Si};
-                                C += K[{Oi}+i];
-                  '''.format(Si=Si, Oi=Oi) for (Oi,Si) in zip(offsets, sizes))
-        forward_callbacks = {
-            'post': 
-                """
-                void post_callback(__global void* output, 
-                                  const uint offset,                      
-                                  __global void* userdata,
-                                  {fp}2 res)
-                    {{                                                            
-                        __global {fp}2* out = (__global {fp}2*) output; 
-                        __global {fp}*  K   = (__global {fp}*)  userdata;
-                        if (offset==0) {{
-                            res = ({fp}2)(0);
-                        }}
-                        else {{
-                            {fp} C = ({fp})(0);
-                            {{
-                                uint i;
-                                uint off = offset;
-                                {gencode}
-                            }}
-                            res /= C;
-                        }}
-                        out[{base_offset}+offset] = res;
-                    }}
-                """.format(fp=fp, gencode=gencode,
-                            base_offset=plan.output_buffer_offset)
-        }
-        user_data = self.Kd.data
-        return (forward_callbacks, user_data)
-
-    def _build_ghost_exchanger(self):
+        poisson_filters = ()
+        for kname in self._kernel_names:
+            kernel, _ = self.symbolic_kernels[kname]
+            kernel = functools.partial(kernel, queue=self.cl_env.default_queue)
+            poisson_filters += (kernel,)
+        self._poisson_filters = poisson_filters
         self._exchange_ghosts = self.dFout.exchange_ghosts(build_launcher=True)
 
     @op_apply
     def apply(self, **kwds):
         """Solve the Poisson equation."""
-        for (fp,bp) in zip(self.forward_plans, self.backward_plans):
-            evt, = fp.enqueue()
-            evt, = bp.enqueue()
-        if (self._exchange_ghosts is not None):
-            evt = self._exchange_ghosts(queue=self.queue)
+        super(OpenClPoisson, self).apply(**kwds)
+        exchange_ghosts = self._exchange_ghosts
+        for (Ft,Bt,filter_poisson) in zip(
+                self.forward_transforms,
+                self.backward_transforms,
+                self._poisson_filters):
+            evt = Ft()
+            evt = filter_poisson()
+            evt = Bt()
+        if exchange_ghosts:
+            evt = exchange_ghosts()
diff --git a/hysop/backend/device/opencl/operator/poisson_curl.py b/hysop/backend/device/opencl/operator/poisson_curl.py
new file mode 100644
index 0000000000000000000000000000000000000000..aa57478c768a7490260f0deec8f88c510aef9e4e
--- /dev/null
+++ b/hysop/backend/device/opencl/operator/poisson_curl.py
@@ -0,0 +1,198 @@
+import primefac, functools
+from hysop import vprint
+from hysop.tools.numpywrappers import npw
+from hysop.tools.decorators  import debug
+from hysop.tools.warning import HysopWarning
+from hysop.tools.units import bytes2str
+from hysop.tools.numerics import is_complex, find_common_dtype
+from hysop.operator.base.poisson_curl import SpectralPoissonCurlOperatorBase
+from hysop.backend.device.opencl.opencl_symbolic import OpenClSymbolic
+from hysop.core.graph.graph import op_apply
+from hysop.core.memory.memory_request import MemoryRequest
+from hysop.backend.device.opencl.opencl_fft import OpenClFFT
+from hysop.backend.device.codegen.base.variables import dtype_to_ctype
+from hysop.constants import FieldProjection
+from hysop.symbolic import local_indices_symbols
+from hysop.symbolic.misc import Select, Expand
+from hysop.symbolic.complex import ComplexMul
+from hysop.symbolic.relational import Assignment, LogicalEQ, LogicalAND
+
+class OpenClPoissonCurl(SpectralPoissonCurlOperatorBase, OpenClSymbolic):
+    '''
+    Solves the poisson-rotational equation using clFFT.
+    '''
+    
+    def __init__(self, **kwds):
+        super(OpenClPoissonCurl, self).__init__(**kwds)
+        dim   = self.dim
+        wcomp = self.W.nb_components
+        assert (dim in (2,3)), dim
+
+        # request the poisson rotational kernel
+            
+        W_Ft = self.W_forward_transforms
+        U_Bt = self.U_backward_transforms
+        W_Bt = self.W_backward_transforms
+        kd1s = self.kd1s
+        kd2s = self.kd2s
+
+        Win  = tuple(Ft.output_symbolic_array('{}in_hat'.format(Ft.field.var_name)) for Ft in W_Ft)
+        Wout = tuple(Bt.input_symbolic_array('{}out_hat'.format(Bt.field.var_name)) if (Bt is not None) else None for Bt in W_Bt)
+        Uout = tuple(Bt.input_symbolic_array('{}out_hat'.format(Bt.field.var_name)) for Bt in U_Bt)
+        K    = tuple(tuple(self.tg._indexed_wave_numbers[kd] for kd in kd1[::-1]) for kd1 in kd1s)
+        KK   = tuple(tuple(self.tg._indexed_wave_numbers[kd] for kd in kd2[::-1]) for kd2 in kd2s)
+
+        def mul(Ki, other):
+            if Ki.Wn.is_complex:
+                return ComplexMul(Ki, other)
+            else:
+                return Ki*other
+
+        if self.should_diffuse:
+            nu, dt = self.nu.s, self.dt.s
+            for i,(win,wout,KKi) in enumerate(zip(Win,Wout,KK)):
+                F = sum(KKi)
+                expr = Assignment(wout, win / (1 - nu*dt*F))
+                self.require_symbolic_kernel('diffusion_kernel__{}'.format(i), expr)
+            Win = Wout
+        
+        indices = local_indices_symbols[:dim]
+        cond = LogicalAND(*tuple(LogicalEQ(idx,0) for idx in indices))
+
+        if self.should_project:
+            exprs = ()
+            dtype = find_common_dtype(*tuple(Ft.output_dtype for Ft in self.W_forward_transforms))
+            Cs = self.symbolic_tmp_scalars('C', dtype=dtype, count=3)
+            for i in xrange(3):
+                expr = 0
+                for j in xrange(3):
+                    e = Win[j]
+                    if (i==j):
+                        e = KK[j][j]*e
+                    else:
+                        e = (ComplexMul(K[j][j], e) if K[j][j].Wn.is_complex else K[j][j]*e)
+                        e = (ComplexMul(K[j][i], e) if K[j][i].Wn.is_complex else K[j][i]*e)
+                    expr += e
+                expr /= sum(KK[i])
+                expr = Select(expr, 0, cond)
+                expr = Assignment(Cs[i], expr)
+                exprs += (expr,)
+            for i in xrange(3):
+                expr = Assignment(Wout[i], Win[i]-Cs[i])
+                exprs += (expr,)
+            self.require_symbolic_kernel('projection_kernel', *exprs)
+            Win = Wout
+
+        exprs = ()
+        for i in xrange(wcomp):
+            F = sum(KK[i])
+            expr = Assignment(Win[i], Select(Win[i]/F,0,cond))
+            self.require_symbolic_kernel('poisson_kernel__{}'.format(i), expr)
+        
+        if (dim == 2):
+            assert wcomp==1
+            e0 = Assignment(Uout[0], -mul(K[0][1], Win[0]))
+            e1 = Assignment(Uout[1], +mul(K[0][0], Win[0]))
+        elif (dim==3):
+            assert wcomp==3
+            e0 = Assignment(Uout[0], mul(K[1][2], Win[1]) - mul(K[2][1], Win[2]))
+            e1 = Assignment(Uout[1], mul(K[2][0], Win[2]) - mul(K[0][2], Win[0]))
+            e2 = Assignment(Uout[2], mul(K[0][1], Win[0]) - mul(K[1][0], Win[1]))
+        else:
+            msg='dim={}'.format(dim)
+            raise NotImplementedError(msg)
+        self.require_symbolic_kernel('curl_kernel__0', e0)
+        self.require_symbolic_kernel('curl_kernel__1', e1)
+        if (dim==3):
+            self.require_symbolic_kernel('curl_kernel__2', e2)
+    
+
+    @debug
+    def setup(self, work):
+        super(OpenClPoissonCurl, self).setup(work)
+        self._build_diffusion_kernel()
+        self._build_projection_kernel()
+        self._build_poisson_curl_kernel()
+        self._build_ghost_exchangers()
+    
+    def _build_diffusion_kernel(self):
+        if self.should_diffuse:
+            diffusion_filters = ()
+            for i in xrange(self.W.nb_components):
+                knl, knl_kwds = \
+                        self.symbolic_kernels['diffusion_kernel__{}'.format(i)]
+                knl = functools.partial(knl, queue=self.cl_env.default_queue)
+                def F(knl=knl, knl_kwds=knl_kwds):
+                    return knl(**knl_kwds())
+                diffusion_filters += (F,)
+            self.diffusion_filters = diffusion_filters
+        else:
+            self.diffusion_filters = None
+
+    def _build_poisson_curl_kernel(self):
+        poisson_filters = ()
+        for i in xrange(self.W.nb_components):
+            knl, __ = self.symbolic_kernels['poisson_kernel__{}'.format(i)] 
+            Fi = functools.partial(knl, queue=self.cl_env.default_queue)
+            poisson_filters += (Fi,)
+        
+        curl_filters = ()
+        for i in xrange(self.U.nb_components):
+            knl, __ = self.symbolic_kernels['curl_kernel__{}'.format(i)] 
+            Fi = functools.partial(knl, queue=self.cl_env.default_queue)
+            curl_filters += (Fi,)
+        
+        self.poisson_filters = poisson_filters
+        self.curl_filters = curl_filters
+
+    def _build_projection_kernel(self):
+        if self.should_project:
+            knl, _ = self.symbolic_kernels['projection_kernel']
+            self.filter_projection = functools.partial(knl, queue=self.cl_env.default_queue)
+        else:
+            self.filter_projection = None
+
+    def _build_ghost_exchangers(self):
+        def noop():
+            pass
+
+        exchange_U_ghosts = self.dU.exchange_ghosts(build_launcher=True)
+        if (exchange_U_ghosts is not None):
+            self.exchange_U_ghosts = functools.partial(exchange_U_ghosts, 
+                    queue=self.cl_env.default_queue)
+        else:
+            self.exchange_U_ghosts = noop
+
+        if (self.should_project or self.should_diffuse):
+            exchange_W_ghosts = self.dW.exchange_ghosts(build_launcher=True)
+            if (exchange_W_ghosts is not None):
+                self.exchange_W_ghosts = functools.partial(exchange_W_ghosts, 
+                        queue=self.cl_env.default_queue)
+            else:
+                self.exchange_W_ghosts = noop
+
+    @op_apply
+    def apply(self, simulation, **kwds):
+        '''Solve the PoissonCurl equation.'''
+        
+        diffuse = self.should_diffuse
+        project = self.do_project(simu=simulation)
+        
+        for Ft in self.W_forward_transforms:
+            evt = Ft()
+        if diffuse:
+            for Fd in self.diffusion_filters:
+                evt = Fd()
+        if project:
+            evt = self.filter_projection()
+        if (diffuse or project):
+            for Bt in self.W_backward_transforms:
+                evt = Bt()
+            evt = self.exchange_W_ghosts()
+        for Fp in self.poisson_filters:
+            evt = Fp()
+        for (Fc, Bt) in zip(self.curl_filters, self.U_backward_transforms):
+            evt = Fc()
+            evt = Bt()
+        evt = self.exchange_U_ghosts()
+
diff --git a/hysop/backend/device/opencl/operator/poisson_rotational.py b/hysop/backend/device/opencl/operator/poisson_rotational.py
deleted file mode 100644
index 4e9eef0f38e50bba0643ebfd5a42c04444141d29..0000000000000000000000000000000000000000
--- a/hysop/backend/device/opencl/operator/poisson_rotational.py
+++ /dev/null
@@ -1,247 +0,0 @@
-import primefac
-from hysop import vprint
-from hysop.tools.numpywrappers import npw
-from hysop.tools.decorators  import debug
-from hysop.tools.warning import HysopWarning
-from hysop.tools.units import bytes2str
-from hysop.operator.base.poisson_rotational import PoissonRotationalOperatorBase
-from hysop.backend.device.opencl.opencl_symbolic import OpenClSymbolic
-from hysop.core.graph.graph import op_apply
-from hysop.core.memory.memory_request import MemoryRequest
-from hysop.backend.device.opencl.opencl_fft import OpenClFFT
-from hysop.backend.device.codegen.base.variables import dtype_to_ctype
-from hysop.constants import FieldProjection
-from hysop.symbolic import local_indices_symbols
-from hysop.symbolic.misc import Select, Expand
-from hysop.symbolic.complex import ComplexMul
-from hysop.symbolic.relational import Assignment, LogicalEQ, LogicalAND
-
-class OpenClPoissonRotational(PoissonRotationalOperatorBase, OpenClSymbolic):
-    '''
-    Solves the poisson-rotational equation using clFFT.
-    '''
-    
-    def initialize(self, **kwds):
-        dim   = self.dim
-        wcomp = self.W.nb_components
-        assert (dim in (2,3)), dim
-        
-        # TODO fix global indices
-        I  = local_indices_symbols[:3]
-        K  = self.symbolic_buffers('K',  count=dim)
-        K2 = self.symbolic_buffers('K2', count=dim)
-        U  = self.symbolic_arrays('U', count=dim, dim=dim)
-        W  = self.symbolic_arrays('W', count=dim, dim=dim)
-        psi = self.symbolic_tmp_scalars('psi', count=wcomp, dtype=self.ctype)
-        ik2, = self.symbolic_tmp_scalars('ik2', dtype=self.dtype)
-        cond  = LogicalAND(*tuple(LogicalEQ(I[i],0) for i in range(dim)))
-        select = lambda expr: Select(expr, 0, cond)
-        laplacian = sum(K2[i][I[i]] for i in range(dim))
-        
-        # request the projection kernel if required
-        if (self.projection != FieldProjection.NONE):
-            assert (dim == 3), dim
-            divW, = self.symbolic_tmp_scalars('divW', dtype=self.ctype)
-
-            expr = Assignment(divW, select(sum(ComplexMul(K[i][I[i]],W[i]) for i in range(3)) / laplacian))
-            exprs = (expr,)
-            
-            for i in xrange(3):
-                correction = W[i] - ComplexMul(K[i][I[i]], divW)
-                expr = Assignment(W[i], correction)
-                exprs += (expr,)
-            self.require_symbolic_kernel('solenoidal_projection_W', *exprs)
-        
-        # request the velocity kernel
-        exprs = ()
-
-        e = Assignment(ik2, select(1/laplacian))
-        exprs += (e,)
-        for (Ui,psi_i) in zip(U[:wcomp],psi[:wcomp]):
-            e = Assignment(psi_i, Ui*ik2)
-            exprs += (e,)
-        if (dim == 2):
-            e0 = Assignment(U[0], ComplexMul(-K[1][I[1]], psi[0]))
-            e1 = Assignment(U[1], ComplexMul(+K[0][I[0]], psi[0]))
-            exprs += (e0, e1)
-        elif (dim==3):
-            e0 = Assignment(U[0], ComplexMul(K[2][I[2]], psi[1]) - ComplexMul(K[1][I[1]], psi[2]))
-            e1 = Assignment(U[1], ComplexMul(K[0][I[0]], psi[2]) - ComplexMul(K[2][I[2]], psi[0]))
-            e2 = Assignment(U[2], ComplexMul(K[1][I[1]], psi[0]) - ComplexMul(K[0][I[0]], psi[1]))
-            exprs += (e0, e1, e2)
-        else:
-            msg='dim={}'.format(dim)
-            raise NotImplementedError(msg)
-        self.require_symbolic_kernel('recover_velocity', *exprs)
-        
-        super(OpenClPoissonRotational, self).initialize(**kwds)
-        
-        self.kernel_buffers = (K,K2,U,W)
-    
-    def generate_wave_numbers(self, dim, resolution, length, dtype, ctype, axes):
-        if (dim>3):
-            msg='clFFT only support 1D, 2D or 3D plans, got a {}D domain.'
-            msg=msg.format(dim)
-            raise ValueError(msg)
-
-        valid_factors = {2,3,5,7,11,13}
-        for Ni in resolution:
-            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(resolution, Ni, factorization, candidates)
-                raise ValueError(msg)
-
-        K1 = ()
-        K2 = ()
-        for i in axes:
-            Ni = resolution[i]
-            Li = length[i]
-
-            if (i==dim-1):
-                k1 = 2*npw.pi*1j*npw.fft.rfftfreq(Ni,Li)*Ni
-            else:
-                k1 = 2*npw.pi*1j*npw.fft.fftfreq(Ni, Li)*Ni
-            k1 = k1.astype(dtype=ctype).copy()
-            k2 = (k1**2).real.astype(dtype=dtype).copy()
-            K1 += (k1,)
-            K2 += (k2,)
-        
-        shape = (sum(k.size for k in K1),)
-        K1d = self.backend.empty(shape=shape, dtype=ctype)
-        K2d = self.backend.empty(shape=shape, dtype=dtype)
-
-        Ksizes, Koffsets = (), ()
-        start, end = 0,0
-        for i,(k1,k2) in enumerate(zip(K1,K2)):
-            assert k1.size == k2.size
-            Koffsets += (start,)
-            Ksizes   += (k1.size,)
-            end      += k1.size
-            K1d[start:end] = k1
-            K2d[start:end] = k2
-            self.kernel_buffers[0][i].bind_memory_object(K1d[start:end])
-            self.kernel_buffers[1][i].bind_memory_object(K2d[start:end])
-            start = end
-        self.K1d      = K1d
-        self.K2d      = K2d
-        self.Ksizes   = Ksizes
-        self.Koffsets = Koffsets
-    
-    @debug
-    def get_work_properties(self):
-        requests  = super(OpenClPoissonRotational,self).get_work_properties()
-        
-        axes  = self.axes
-        dU    = self.dU
-        ctype = self.ctype
-
-        shape = list(self.resolution)
-        shape[axes[0]] = shape[axes[0]] // 2 + 1
-        shape = tuple(shape)
-        assert npw.array_equal(shape, self.Ksizes[::-1])
-        self.cshape = shape
-        self.csize = npw.prod(shape, dtype=npw.int64)
-        
-        for i in xrange(self.dim):
-            request = MemoryRequest.empty_like(a=dU[i], shape=shape, dtype=ctype)
-            requests.push_mem_request('R2C_C2R_{}'.format(i), request)
-        return requests
-
-    @debug
-    def setup(self, work):
-        fft_buffers = tuple(work.get_buffer(self, 'R2C_C2R_{}'.format(i))[0]
-                                for i in xrange(self.dim))
-        for i in xrange(self.dim):
-            self.kernel_buffers[2][i].bind_memory_object(fft_buffers[i])
-            self.kernel_buffers[3][i].bind_memory_object(fft_buffers[i])
-        super(OpenClPoissonRotational, self).setup(work)
-        self._build_velocity_kernel()
-        self._build_projection_kernel()
-        self._build_fft_plans(fft_buffers)
-        self._build_ghost_exchangers()
-
-    def _build_velocity_kernel(self):
-        self.compute_velocity_kernel, _ = self.symbolic_kernels['recover_velocity']
-
-    def _build_projection_kernel(self):
-        if (self.projection != FieldProjection.NONE):
-            self.projection_kernel, _ = self.symbolic_kernels['solenoidal_projection_W']
-
-    def _build_fft_plans(self, fft_buffers):
-        axes    = self.axes
-        context = self.backend.cl_env.context
-        queue   = self.backend.cl_env.default_queue
-        
-        forward_W_plans, backward_U_plans, backward_W_plans = [],[],[]
-        
-        for (i,Wi) in enumerate(self.W_buffers):
-            fp = OpenClFFT(context=context, queue=queue, 
-                        in_array=Wi, out_array=fft_buffers[i].handle,
-                        axes=axes, fast_math=False,
-                        real=True)
-            fp.bake()
-            forward_W_plans.append(fp)
-
-            if (self.projection != FieldProjection.NONE):
-                bp = OpenClFFT(context=context, queue=queue, 
-                            in_array=fft_buffers[i].handle, out_array=Wi,
-                            axes=axes, fast_math=False, real=True)
-                bp.bake()
-                backward_W_plans.append(bp)
-
-        for (i,Ui) in enumerate(self.U_buffers):
-            bp = OpenClFFT(context=context, queue=queue, 
-                        in_array=fft_buffers[i].handle, out_array=Ui,
-                        axes=axes, fast_math=False,
-                        real=True)
-            bp.bake()
-            backward_U_plans.append(bp)
-            
-        all_plans = forward_W_plans + backward_W_plans + backward_U_plans 
-        tmp_buffer = OpenClFFT.allocate_plans(self, all_plans)
-
-        self.forward_W_plans  = forward_W_plans
-        self.backward_W_plans = backward_W_plans
-        self.backward_U_plans = backward_U_plans
-        self.fft_buffers = fft_buffers
-        self.tmp_buffer = tmp_buffer
-        self.queue      = queue
-
-    def _build_ghost_exchangers(self):
-        self._exchange_U_ghosts = self.dU.exchange_ghosts(build_launcher=True)
-        if (self.projection != FieldProjection.NONE):
-            self._exchange_W_ghosts = self.dW.exchange_ghosts(build_launcher=True)
-
-    @op_apply
-    def apply(self, simulation, **kwds):
-        '''Solve the PoissonRotational equation.'''
-        # /!\ clFFT use the destination buffer as a scratch
-        # so we reverse the order of forward transforms.
-        
-        for fp in self.forward_W_plans:
-            evt, = fp.enqueue(queue=self.queue)
-        
-        # project and recover vorticity if required
-        if self._do_project(simulation):
-            evt = self.projection_kernel(queue=self.queue)
-            for bp in self.backward_W_plans:
-                evt, = bp.enqueue()
-            if (self._exchange_W_ghosts is not None):
-                evt = self._exchange_W_ghosts(queue=self.queue)
-        
-        # recover velocity
-        evt = self.compute_velocity_kernel(queue=self.queue)
-        
-        for bp in self.backward_U_plans:
-            evt, = bp.enqueue()
-
-        if (self._exchange_U_ghosts is not None):
-            evt = self._exchange_U_ghosts(queue=self.queue)
diff --git a/hysop/backend/device/opencl/operator/solenoidal_projection.py b/hysop/backend/device/opencl/operator/solenoidal_projection.py
index b1be2e6182e74368c984dfcb7576351da08c9ce8..db614290ed6f0aeb15c6f3a656b4301ad9398070 100644
--- a/hysop/backend/device/opencl/operator/solenoidal_projection.py
+++ b/hysop/backend/device/opencl/operator/solenoidal_projection.py
@@ -1,8 +1,9 @@
-import primefac
+import primefac, functools
 from hysop import vprint
 from hysop.tools.numpywrappers import npw
 from hysop.tools.decorators  import debug
 from hysop.tools.units import bytes2str
+from hysop.tools.numerics import is_complex, find_common_dtype
 from hysop.operator.base.solenoidal_projection import SolenoidalProjectionOperatorBase
 from hysop.backend.device.opencl.opencl_symbolic import OpenClSymbolic
 from hysop.core.graph.graph import op_apply
@@ -22,186 +23,81 @@ class OpenClSolenoidalProjection(SolenoidalProjectionOperatorBase, OpenClSymboli
     
     def initialize(self, **kwds):
         # request the projection kernel if required
-        assert (self.dim == 3), self.dim
-        I     = local_indices_symbols[:3]
-        K     = self.symbolic_buffers('K',  count=3)
-        K2    = self.symbolic_buffers('K2', count=3)
-
-        F     = self.symbolic_arrays('F', count=3, dim=3)
-        tmp,  = self.symbolic_tmp_scalars('tmp', dtype=self.ctype)
-        tmp_F = self.symbolic_tmp_scalars('Ftmp', dtype=self.ctype, count=3)
-        
-        S0 = sum(ComplexMul(K[i][I[i]],F[i]) for i in range(3))
-        S1 = sum(K2[i][I[i]] for i in range(3))
-        S2 = sum(ComplexMul(K[i][I[i]],tmp_F[i]) for i in range(3))
+        Fin  = tuple(Ft.output_symbolic_array('Fin{}_hat'.format(i))
+                    for (i,Ft) in enumerate(self.forward_transforms))
+        Fout = tuple(Bt.input_symbolic_array('Fout{}_hat'.format(i))
+                    for (i,Bt) in enumerate(self.backward_transforms))
+        K1s, K2s = (), ()
+        for kd1 in self.kd1s:
+            Ki = self.tg.indexed_wavenumbers(*kd1)[::-1]
+            K1s += (Ki,)
+        for kd2 in self.kd2s:
+            Ki = self.tg.indexed_wavenumbers(*kd2)[::-1]
+            K2s += (Ki,)
+
+        dtype = find_common_dtype(*tuple(Ft.output_dtype for Ft in self.forward_transforms))
+        Cs = self.symbolic_tmp_scalars('C', dtype=dtype, count=3)
         
-        exprs = ( Assignment(tmp, S0), )
-
-        if self.compute_divFin:
-            divFin, = self.symbolic_arrays('divFin', count=1, dim=3)
-            exprs += ( Assignment(divFin, tmp), )
-        else:
-            divFin = None
-
-        exprs += ( Assignment(tmp, tmp / S1), )
+        I = local_indices_symbols[:3]
+        cond  = LogicalAND(*tuple(LogicalEQ(Ik,0) for Ik in I))
         
-        cond = LogicalAND(*tuple(LogicalEQ(I[i],0) for i in range(3)))
+        exprs = ()
         for i in xrange(3):
-            correction = F[i] - Select(ComplexMul(K[i][I[i]], tmp), 0, cond)
-            expr   = Assignment(tmp_F[i], correction)
+            expr = 0
+            for j in xrange(3):
+                e = Fin[j]
+                if (i==j):
+                    e = K2s[j][j]*e
+                else:
+                    e = (ComplexMul(K1s[j][j], e) if K1s[j][j].Wn.is_complex else K1s[j][j]*e)
+                    e = (ComplexMul(K1s[j][i], e) if K1s[j][i].Wn.is_complex else K1s[j][i]*e)
+                expr += e
+            expr /= sum(K2s[i])
+            expr = Select(expr, 0, cond)
+            expr = Assignment(Cs[i], expr)
             exprs += (expr,)
-        
-        if self.compute_divFout:
-            divFout, = self.symbolic_arrays('divFout', count=1, dim=3)
-            exprs += ( Assignment(divFout, S2), )
-        else:
-            divFout = None
-
         for i in xrange(3):
-            expr   = Assignment(F[i], tmp_F[i])
+            expr = Assignment(Fout[i], Fin[i]-Cs[i])
             exprs += (expr,)
-        
-        self.require_symbolic_kernel('solenoidal_projection', *exprs)
-        self.projection_buffers = (K,K2,F,divFin,divFout)
-        super(OpenClSolenoidalProjection, self).initialize(**kwds)
-    
-    def generate_wave_numbers(self, dim, resolution, length, dtype, ctype, axes):
-        if (dim>3):
-            msg='clFFT only support 1D, 2D or 3D plans, got a {}D domain.'
-            msg=msg.format(dim)
-            raise ValueError(msg)
-
-        valid_factors = {2,3,5,7,11,13}
-        for Ni in resolution:
-            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(resolution, Ni, factorization, candidates)
-                raise ValueError(msg)
-
-        K1 = ()
-        K2 = ()
-        for i in axes:
-            Ni = resolution[i]
-            Li = length[i]
 
-            if (i==dim-1):
-                k1 = 2*npw.pi*1j*npw.fft.rfftfreq(Ni,Li)*Ni
-            else:
-                k1 = 2*npw.pi*1j*npw.fft.fftfreq(Ni, Li)*Ni
-            k1 = k1.astype(dtype=ctype).copy()
-            k2 = (k1**2).real.astype(dtype=dtype).copy()
-            K1 += (k1,)
-            K2 += (k2,)
-        
-        shape = (sum(k.size for k in K1),)
-        K1d = self.backend.empty(shape=shape, dtype=ctype)
-        K2d = self.backend.empty(shape=shape, dtype=dtype)
+        self.require_symbolic_kernel('solenoidal_projection', *exprs)
 
-        Ksizes, Koffsets = (), ()
-        start, end = 0,0
-        for i,(k1,k2) in enumerate(zip(K1,K2)):
-            assert k1.size == k2.size
-            Koffsets += (start,)
-            Ksizes   += (k1.size,)
-            end      += k1.size
-            K1d[start:end] = k1
-            K2d[start:end] = k2
-            self.projection_buffers[0][i].bind_memory_object(K1d[start:end])
-            self.projection_buffers[1][i].bind_memory_object(K2d[start:end])
-            start = end
-        
-        self.K1d      = K1d
-        self.K2d      = K2d
-        self.Ksizes   = Ksizes
-        self.Koffsets = Koffsets
-    
-    @debug
-    def get_work_properties(self):
-        requests  = super(OpenClSolenoidalProjection,self).get_work_properties()
+        if self.compute_divFin:
+            divFin = self.backward_divFin_transform.input_symbolic_array('divFin')
+            expr = sum(ComplexMul(K1s[j][j],Fin[j]) if K1s[j][j].Wn.is_complex else K1s[j][j]*Fin[j] 
+                            for j in xrange(3))
+            expr = Assignment(divFin, expr)
+            self.require_symbolic_kernel('compute_divFin', expr)
         
-        axes  = self.axes
-        dFin  = self.dFin
-        ctype = self.ctype
+        if self.compute_divFout:
+            expr = sum(ComplexMul(K1s[j][j],Fout[j]) if K1s[j][j].Wn.is_complex else K1s[j][j]*Fout[j] 
+                            for j in xrange(3))
+            divFout = self.backward_divFout_transform.input_symbolic_array('divFout')
+            expr = Assignment(divFout, expr)
+            self.require_symbolic_kernel('compute_divFout', expr)
 
-        shape = list(self.resolution)
-        shape[axes[0]] = shape[axes[0]] // 2 + 1
-        shape = tuple(shape)
-        assert npw.array_equal(shape, self.Ksizes[::-1])
-        
-        nb_components = dFin.nb_components + self.compute_divFin + self.compute_divFout
-        request = MemoryRequest.empty_like(a=dFin, dtype=ctype, 
-                shape=shape,
-                nb_components=nb_components)
-        requests.push_mem_request('R2C_C2R', request)
-        return requests
+        super(OpenClSolenoidalProjection, self).initialize(**kwds)
+   
 
     @debug
     def setup(self, work):
-        fft_buffers = work.get_buffer(self, 'R2C_C2R')
-        i=0
-        for buf in self.projection_buffers[2]:
-            buf.bind_memory_object(fft_buffers[i])
-            i+=1
-        if self.compute_divFin: 
-            self.projection_buffers[3].bind_memory_object(fft_buffers[i])
-            i+=1
-        if self.compute_divFout:
-            self.projection_buffers[4].bind_memory_object(fft_buffers[i])
-            i+=1
-        assert i==len(fft_buffers)
         super(OpenClSolenoidalProjection, self).setup(work)
         self._build_projection_kernel()
-        self._build_fft_plans(fft_buffers)
+        self._build_divergence_kernels()
         self._build_ghost_exchangers()
 
     def _build_projection_kernel(self):
-        self.projection_kernel, _ = self.symbolic_kernels['solenoidal_projection']
-
-    def _build_fft_plans(self, fft_buffers):
-        axes    = self.axes
-        context = self.backend.cl_env.context
-        queue   = self.backend.cl_env.default_queue
-        
-        forward_plans, backward_plans = [], []
-        
-        for (i,Fin) in enumerate(self.Fin_buffers):
-            fp = OpenClFFT(context=context, queue=queue, 
-                        in_array=Fin, out_array=fft_buffers[i],
-                        axes=axes, fast_math=False,
-                        real=True)
-            fp.bake()
-            forward_plans.append(fp)
-        
-        i=0
-        for Fout in (self.Fout_buffers + self.divFin_buffers + self.divFout_buffers):
-            if (Fout is None):
-                continue
-            bp = OpenClFFT(context=context, queue=queue, 
-                        in_array=fft_buffers[i], out_array=Fout,
-                        axes=axes, fast_math=False,
-                        real=True)
-            bp.bake()
-            backward_plans.append(bp)
-            i+=1
-        assert i==len(fft_buffers)
-            
-        all_plans = forward_plans + backward_plans
-        tmp_buffer = OpenClFFT.allocate_plans(self, all_plans)
-
-        self.forward_plans  = forward_plans
-        self.backward_plans = backward_plans
-        self.fft_buffers    = fft_buffers
-        self.tmp_buffer     = tmp_buffer
-        self.queue          = queue
+        knl, _ = self.symbolic_kernels['solenoidal_projection']
+        self.projection_kernel = functools.partial(knl, queue=self.cl_env.default_queue)
 
+    def _build_divergence_kernels(self):
+        if self.compute_divFin:
+            knl, _ = self.symbolic_kernels['compute_divFin']
+            self.compute_divFin_kernel = functools.partial(knl, queue=self.cl_env.default_queue)
+        if self.compute_divFout:
+            knl, _ = self.symbolic_kernels['compute_divFout']
+            self.compute_divFout_kernel = functools.partial(knl, queue=self.cl_env.default_queue)
+    
     def _build_ghost_exchangers(self):
         kl = OpenClKernelListLauncher(name='exchange_ghosts')
         kl += self.dFout.exchange_ghosts(build_launcher=True)
@@ -209,15 +105,24 @@ class OpenClSolenoidalProjection(SolenoidalProjectionOperatorBase, OpenClSymboli
             kl += self.ddivFin.exchange_ghosts(build_launcher=True)
         if self.compute_divFout:
             kl += self.ddivFout.exchange_ghosts(build_launcher=True)
-        self._exchange_ghosts = kl
+        self.exchange_ghost_kernels = functools.partial(kl, queue=self.cl_env.default_queue)
+
 
     @op_apply
-    def apply(self, simulation, **kwds):
+    def apply(self, simulation=None, **kwds):
         '''Solve the SolenoidalProjection.'''
-        for fp in self.forward_plans[::-1]:
-            evt, = fp.enqueue()
-        evt = self.projection_kernel(queue=self.queue)
-        for bp in self.backward_plans:
-            evt, = bp.enqueue()
-        evt = self._exchange_ghosts(queue=self.queue)
+        super(OpenClSolenoidalProjection, self).apply(**kwds)
+
+        for Ft in self.forward_transforms:
+            evt = Ft()
+        if self.compute_divFin:
+            evt = self.compute_divFin_kernel()
+            evt = self.backward_divFin_transform()
+        evt = self.projection_kernel()
+        if self.compute_divFout:
+            evt = self.compute_divFout_kernel()
+            evt = self.backward_divFout_transform()
+        for Bt in self.backward_transforms:
+            evt = Bt()
+        evt = self.exchange_ghost_kernels()
 
diff --git a/hysop/backend/device/opencl/operator/transpose.py b/hysop/backend/device/opencl/operator/transpose.py
index fb66c65b1b709c3b1ab1ca0a3b78bf352916f8d2..9e513fe471b7b4856dcebef1060609050bcd3bf0 100644
--- a/hysop/backend/device/opencl/operator/transpose.py
+++ b/hysop/backend/device/opencl/operator/transpose.py
@@ -43,7 +43,9 @@ class OpenClTranspose(TransposeOperatorBase, OpenClOperator):
         hardcode_arrays = (compute_inplace or not is_inplace)
         transpose, _ = kernel.autotune(axes=axes, 
                 hardcode_arrays=hardcode_arrays,
-                is_inplace=compute_inplace, input_field=input_field, output_field=output_field)
+                is_inplace=compute_inplace, 
+                input_buffer=input_field.sbuffer, 
+                output_buffer=output_field.sbuffer)
         
         launcher = OpenClKernelListLauncher(name=transpose.name)
         for i in xrange(self.nb_components):
diff --git a/hysop/backend/host/fortran/operator/diffusion.py b/hysop/backend/host/fortran/operator/diffusion.py
index 36c010549b9f4b74c4ae4d4e26540a7f697ac094..5a9bf4749c7e57ff8fa646596f8700abb7690213 100644
--- a/hysop/backend/host/fortran/operator/diffusion.py
+++ b/hysop/backend/host/fortran/operator/diffusion.py
@@ -6,12 +6,71 @@ from hysop.fields.continuous_field import Field
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
 from hysop.parameters.scalar_parameter import ScalarParameter
 from hysop.core.graph.graph import op_apply
-from hysop.operator.base.diffusion import DiffusionBase
 
-class DiffusionFFTW(DiffusionBase, FortranFFTWOperator):
+class DiffusionFFTW(FortranFFTWOperator):
+
+    @debug
+    def __init__(self, Fin, Fout,
+                       nu, variables, dt, **kargs):
+        """Diffusion operator base.
+
+        Parameters
+        ----------
+        Fin : :class:`~hysop.fields.continuous_field.Field`
+            The input field to be diffused.
+        Fout:  :class:`~hysop.fields.continuous_field.Field`
+            The output field to be diffused.
+        variables: dictionary of fields:topology
+            The choosed discretizations.
+        nu : float or ScalarParameter.
+            nu value.
+        dt: ScalarParameter
+            Timestep parameter that will be used for time integration.
+        kargs:
+            Base class parameters.
+
+        Notes:
+            *Equations:
+                dF/dt = nu*Laplacian(F)
+                in  = Win
+                out = Wout
+
+            *Implicit resolution in Fourier space:
+                F_hat(tn+1) = 1/(1-nu*dt*sum(Ki**2)) * F_hat(tn)
+        """
+        check_instance(Fin,  Field)
+        check_instance(Fout, Field)
+        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
+        check_instance(nu, (float, ScalarParameter))
+        check_instance(dt, ScalarParameter)
+
+        assert Fin.nb_components == Fout.nb_components, \
+                'input and output components mismatch'
+        assert variables[Fin] == variables[Fout], \
+                'input and output topology mismatch'
+        assert Fin.domain is Fout.domain,\
+                'input and output domain mismatch'
+
+        input_fields   = { Fin:  variables[Fin] }
+        output_fields  = { Fout: variables[Fout] }
+        input_params   = { dt.name: dt }
+        if isinstance(nu, ScalarParameter):
+            input_params[nu.name] = nu
+        else:
+            self._real_nu = nu
+            nu = lambda: self._real_nu
+
+        super(DiffusionFFTW, self).__init__(input_fields=input_fields,
+                                            output_fields=output_fields,
+                                            input_params=input_params,
+                                            **kargs)
+
+        self.Fin   = Fin
+        self.Fout  = Fout
+        self.is_inplace = (Fin is Fout)
+        self.nu = nu
+        self.dt = dt
 
-    def __init__(self, **kargs):
-        super(DiffusionFFTW, self).__init__(**kargs)
 
     def initialize(self, **kwds):
         super(DiffusionFFTW,self).initialize(**kwds)
@@ -30,10 +89,18 @@ class DiffusionFFTW(DiffusionBase, FortranFFTWOperator):
         else:
             raise NotImplementedError(str(dim) + "D case not yet implemented.")
 
+    @debug
     def discretize(self):
         if self.discretized:
             return
         super(DiffusionFFTW,self).discretize()
+        dFin  = self.get_input_discrete_field(self.Fin)
+        dFout = self.get_output_discrete_field(self.Fout)
+        assert npw.array_equal(dFin.compute_resolution, dFout.compute_resolution)
+        self.dFin = dFin
+        self.dFout = dFout
+        self.input_buffers = dFin.compute_buffers
+        self.output_buffers = dFout.compute_buffers
         if not self.dFout.has_unique_ghosts():
             msg='Ghosts are not the same in all directions for output field {}.'
             msg=msg.format(self.dFout.short_description())
@@ -45,7 +112,7 @@ class DiffusionFFTW(DiffusionBase, FortranFFTWOperator):
     @op_apply
     def apply(self, **kargs):
         super(DiffusionFFTW,self).apply(**kargs)
-        nudt   = self.dt()*self.viscosity()
+        nudt   = self.dt()*self.nu()
         buffers, ghosts = self.buffers, self.ghosts
         
         if (not self.is_inplace):
diff --git a/hysop/backend/host/fortran/operator/fortran_fftw.py b/hysop/backend/host/fortran/operator/fortran_fftw.py
index 27e0f496d3fc989f126735ddea8c804df1e0cd0f..529453a8875dd22a569249f46141808fb4883b8b 100644
--- a/hysop/backend/host/fortran/operator/fortran_fftw.py
+++ b/hysop/backend/host/fortran/operator/fortran_fftw.py
@@ -8,7 +8,7 @@ except ImportError:
     msg += 'Try to recompile HySoP with WITH_FFTW=ON'
     raise ImportError(msg)
 
-from hysop.constants import HYSOP_ORDER, HYSOP_REAL, TranspositionState
+from hysop.constants import HYSOP_ORDER, HYSOP_REAL, TranspositionState, BoundaryCondition
 from hysop.tools.numpywrappers import npw
 from hysop.tools.decorators import debug
 from hysop.tools.types import check_instance
@@ -35,6 +35,16 @@ class FortranFFTWOperator(FortranOperator):
                     msg+='\nHYSOP_REAL is {} but field {} has dtype {}.'
                     msg=msg.format(HYSOP_REAL.__name__, fi.name, fi.dtype)
                     raise RuntimeError(msg)
+                if (any((bd!=BoundaryCondition.PERIODIC) for bd in fi.lboundaries) or
+                    any((bd!=BoundaryCondition.PERIODIC) for bd in fi.rboundaries)):
+                    msg='FortranFFTW operators only work with PERIODIC boundary conditions:'
+                    msg+='\n  operator:    {}'.format(self.name)
+                    msg+='\n  field:       {}'.format(fi.pretty_name)
+                    msg+='\n  lboundaries: {}'.format(fi.lboundaries)
+                    msg+='\n  rboundaries: {}'.format(fi.rboundaries)
+                    raise RuntimeError(msg)
+
+
 
         domain = self.input_fields.keys()[0].domain
         self.dim      = domain.dim
diff --git a/hysop/backend/host/fortran/operator/poisson.py b/hysop/backend/host/fortran/operator/poisson.py
index 0522c48c35c3552d3d228d2d7a60e91323cd16c6..662be77cfe0bec173a7b6339e32d3086017806d5 100644
--- a/hysop/backend/host/fortran/operator/poisson.py
+++ b/hysop/backend/host/fortran/operator/poisson.py
@@ -76,8 +76,8 @@ class PoissonFFTW(FortranFFTWOperator):
     @op_apply
     def apply(self, **kargs):
         super(PoissonFFTW, self).apply(**kargs)
-        # Vectors are given in ZYX layout to Fortran
         (buf_in, buf_out) = self.buffers
         self._solve(buf_in, buf_out, self.ghosts)
         buf_out[...] *= -1
         self.dFout.exchange_ghosts()
+
diff --git a/hysop/backend/host/fortran/operator/poisson_rotational.py b/hysop/backend/host/fortran/operator/poisson_curl.py
similarity index 82%
rename from hysop/backend/host/fortran/operator/poisson_rotational.py
rename to hysop/backend/host/fortran/operator/poisson_curl.py
index 3a0ca983a2cd4eba6abb8cb31b7594e1dc8a43e5..eb18be4656b00ab9618be8410dd09a762c372fa6 100644
--- a/hysop/backend/host/fortran/operator/poisson_rotational.py
+++ b/hysop/backend/host/fortran/operator/poisson_curl.py
@@ -6,13 +6,13 @@ from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
 from hysop.constants import FieldProjection
 from hysop.backend.host.fortran.operator.fortran_fftw import fftw2py, FortranFFTWOperator
 from hysop.core.graph.graph import op_apply
-from hysop.operator.base.poisson_rotational import PoissonRotationalOperatorBase
+from hysop.operator.base.poisson_curl import PoissonCurlOperatorBase
 import numpy as np
 
-class FortranPoissonRotational(PoissonRotationalOperatorBase, FortranFFTWOperator):
+class FortranPoissonCurl(PoissonCurlOperatorBase, FortranFFTWOperator):
 
     def initialize(self, **kwds):
-        super(FortranPoissonRotational, self).initialize(**kwds)
+        super(FortranPoissonCurl, self).initialize(**kwds)
         dim = self.dim
         if (dim==2):
             self._solve = self._solve_2d
@@ -23,7 +23,7 @@ class FortranPoissonRotational(PoissonRotationalOperatorBase, FortranFFTWOperato
 
     @debug
     def discretize(self):
-        super(FortranPoissonRotational, self).discretize()
+        super(FortranPoissonCurl, self).discretize()
         dU = self.dU
         dW = self.dW
 
@@ -38,8 +38,8 @@ class FortranPoissonRotational(PoissonRotationalOperatorBase, FortranFFTWOperato
 
     @op_apply
     def apply(self, simulation=None, **kargs):
-        super(FortranPoissonRotational, self).apply(simulation=simulation, **kargs)
-        if self._do_project(simulation):
+        super(FortranPoissonCurl, self).apply(simulation=simulation, **kargs)
+        if self.do_project(simulation):
             self._project()
             self.dW.exchange_ghosts()
         self._solve()
diff --git a/hysop/backend/host/fortran/operator/scales_advection.py b/hysop/backend/host/fortran/operator/scales_advection.py
index 00214e566b1bf6307faad006bc4b360ac85e1a45..100845d30cc8d4114c70487d374495522b75b0fa 100644
--- a/hysop/backend/host/fortran/operator/scales_advection.py
+++ b/hysop/backend/host/fortran/operator/scales_advection.py
@@ -298,13 +298,13 @@ class ScalesAdvection(FortranOperator):
         msg0="No ghosts allowed in Scales advection"
         msg1="Scales is only for periodic domains."
         assert (v_topo.ghosts == 0).all(), msg0
-        assert (v_topo.domain.periodicity.all()), msg1
+        assert v_topo.mesh.periodicity.all(), msg1
         for (dfi, dfo) in zip(self.dadvected_fields_in,
                               self.dadvected_fields_out):
             assert (dfi.topology.ghosts == 0).all(), msg0
-            assert (dfi.domain.periodicity.all()), msg1
+            assert (dfi.periodicity.all()), msg1
             assert (dfo.topology.ghosts == 0).all(), msg0
-            assert (dfo.domain.periodicity.all()), msg1
+            assert (dfo.periodicity.all()), msg1
 
         sresol = s_topo.mesh.grid_resolution
         assert (sresol%s_topo.proc_shape == 0).all(),\
diff --git a/hysop/backend/host/host_array_backend.py b/hysop/backend/host/host_array_backend.py
index bd4fcecc1efbdcebadbb5888448b5f4641b3ebb3..734140c5306a4824a57caeedc42a662038e01180 100644
--- a/hysop/backend/host/host_array_backend.py
+++ b/hysop/backend/host/host_array_backend.py
@@ -1,4 +1,6 @@
 
+
+import warnings
 from hysop.deps import  np
 from hysop.constants import Backend
 from hysop.constants import HYSOP_REAL, HYSOP_INTEGER, HYSOP_BOOL
@@ -157,10 +159,10 @@ class HostArrayBackend(ArrayBackend):
         elif isinstance(dst, np.ndarray):
             dst[...] = src
         elif isinstance(dst, OpenClArray):
+            queue = first_not_None(queue, dst.default_queue)
             from hysop.backend.device.opencl.opencl_copy_kernel_launchers \
                     import OpenClCopyBufferRectLauncher
-            queue = first_not_None(queue, dst.default_queue)
-            kl = OpenClCopyBufferRectLauncher.from_slices('buffer',
+            kl = OpenClCopyBufferRectLauncher.from_slices('copyto',
                     src=src, dst=dst)
             evt = kl(queue=queue)
             if async:
@@ -194,7 +196,7 @@ class HostArrayBackend(ArrayBackend):
             dtype=HYSOP_BOOL
             import warning
             msg='HostArrayBackend: numpy bool array converted to hysop_bool={}.'.format(dtype)
-            warning.warn(msg, HysopWarning)
+            warnings.warn(msg, HysopWarning)
         
         if (buf is None):
             assert offset==0
diff --git a/hysop/backend/host/host_operator.py b/hysop/backend/host/host_operator.py
index 21d820dd5154451214aaab05245ef492fa6d0e41..926e604b9c55afda10acfa65f61ae6888cd309d0 100644
--- a/hysop/backend/host/host_operator.py
+++ b/hysop/backend/host/host_operator.py
@@ -6,9 +6,13 @@ discrete operators working on the Host backend.
     opencl backend.
 """
 from abc import ABCMeta
+from contextlib import contextmanager
 from hysop.tools.decorators import debug
+from hysop.tools.types import check_instance, first_not_None
 from hysop.constants import ComputeGranularity, Backend
 from hysop.core.graph.computational_operator import ComputationalGraphOperator
+from hysop.topology.topology_descriptor import TopologyDescriptor
+
 
 class HostOperator(ComputationalGraphOperator):
     """
@@ -25,9 +29,209 @@ class HostOperator(ComputationalGraphOperator):
         Backend.HOST and share the same HostEnvironment.
         """
         super(HostOperator, self).__init__(**kwds)
-
-    def supported_backends(self):
+    
+    @classmethod
+    def supported_backends(cls):
         """
         Return the backends that this operator's topologies can support.
         """
         return set([Backend.HOST])
+
+
+
+class OpenClMappable(object):
+    """
+    Extend host operator capabilities to work on mapped opencl buffers
+    """
+
+    class OpenClMappedMemoryObjectGetter(object):
+        def __init__(self, obj, evt):
+            check_instance(obj, OpenClMappable)
+            self.__obj = obj
+            self.__evt = evt
+        def __getitem__(self, key):
+            return obj.get_mapped_object(key=key)
+        @property
+        def evt(self):
+            return self.__evt
+    
+    @classmethod
+    def supported_backends(cls):
+        sb = super(OpenClMappable, cls).supported_backends()
+        sb.add(Backend.OPENCL)
+        return sb
+    
+    @debug
+    def create_topology_descriptors(self): 
+        if self.enable_opencl_host_buffer_mapping:
+            # enforce opencl topology on host operator
+            for (field, topo_descriptor) in self.input_fields.iteritems():
+                topo_descriptor = TopologyDescriptor.build_descriptor(
+                        backend=Backend.OPENCL,
+                        operator=self,
+                        field=field,
+                        handle=topo_descriptor,
+                        cl_env=self.cl_env)
+                self.input_fields[field] = topo_descriptor
+
+            for (field, topo_descriptor) in self.output_fields.iteritems():
+                topo_descriptor = TopologyDescriptor.build_descriptor(
+                        backend=Backend.OPENCL,
+                        operator=self,
+                        field=field,
+                        handle=topo_descriptor,
+                        cl_env=self.cl_env)
+                self.output_fields[field] = topo_descriptor
+        else:
+            super(OpenClMappable, self).create_topology_descriptors()
+
+    def __init__(self, cl_env=None, mpi_params=None,
+            enable_opencl_host_buffer_mapping=False, **kwds):
+        
+        if enable_opencl_host_buffer_mapping:
+            msg = 'OpenClMappable is an interface dedicated to extend HostOperator.'
+            assert isinstance(self, HostOperator), msg
+
+        if (cl_env is not None):
+            if (mpi_params is None):
+                mpi_params = cl_env.mpi_params
+            else:
+                assert (mpi_params == cl_env.mpi_params)
+
+        super(OpenClMappable, self).__init__(mpi_params=mpi_params, **kwds)
+        
+        self.__cl_env = cl_env
+        self.__enable_opencl_host_buffer_mapping = enable_opencl_host_buffer_mapping
+        
+        self.__mapped = False
+        self.__registered_objects = {}
+        self.__registered_getters = {}
+        self.__mapped_objects     = {}
+
+    def __del__(self):
+        self.unmap_objects()
+
+    @property
+    def cl_env(self):
+        return self.__cl_env
+    
+    @property
+    def enable_opencl_host_buffer_mapping(self):
+        return self.__enable_opencl_host_buffer_mapping
+
+    def setup(self, **kwds):
+        super(OpenClMappable, self).setup(**kwds)
+        self._register_fields()
+
+    def _register_fields(self):
+        from hysop.fields.discrete_field import DiscreteScalarField, DiscreteScalarFieldView
+        ivfields = set(filter(lambda f: f.backend.kind==Backend.OPENCL,  self.input_discrete_fields.values()))
+        ovfields = set(filter(lambda f: f.backend.kind==Backend.OPENCL, self.output_discrete_fields.values()))
+        check_instance(ivfields, set, values=DiscreteScalarFieldView)
+        check_instance(ovfields, set, values=DiscreteScalarFieldView)
+        vfields = ivfields.union(ovfields)
+        if vfields:
+            assert (self.cl_env is not None), 'No opencl environment has been given.'
+            from hysop.backend.device.opencl.opencl_env import OpenClEnvironment
+            check_instance(self.cl_env, OpenClEnvironment)
+
+            from hysop.backend.device.opencl import cl
+            ifields = set(f.dfield for f in ivfields)
+            ofields = set(f.dfield for f in ovfields)
+            check_instance(ifields, set, values=DiscreteScalarField)
+            check_instance(ofields, set, values=DiscreteScalarField)
+            fields = ifields.union(ofields)
+            for field in fields:
+                flags = 0
+                if field in ifields:
+                    flags |= cl.map_flags.READ
+                if field in ofields:
+                    flags |= cl.map_flags.WRITE
+                assert (field._data is not None)
+                self.register_mappable_object(key=field, obj=field._data.handle, 
+                        flags=flags)
+            for vfield in vfields:
+                self.register_data_getter(get_key=vfield, obj_key=vfield.dfield,
+                                            getter=vfield._compute_data_view)
+
+    def register_mappable_object(self, key, obj, flags):
+        from hysop.backend.device.opencl import clArray
+        msg='Device memory object "{}" has already been registered.'
+        msg=msg.format(key)
+        assert (key not in self.__registered_objects), msg
+        check_instance(obj, clArray.Array)
+        self.__registered_objects[key] = (obj, flags)
+    
+    def register_data_getter(self, get_key, obj_key, getter):
+        assert callable(getter)
+        msg='Device memory getter "{}" has already been registered as an object.'
+        msg=msg.format(get_key)
+        assert (get_key not in self.__registered_objects), msg
+        msg='Device memory getter "{}" has already been registered as a getter.'
+        msg=msg.format(get_key)
+        assert (get_key not in self.__registered_getters), msg
+        msg='Device memory object "{}" has not been registered.'
+        msg=msg.format(obj_key)
+        assert (obj_key in self.__registered_objects), msg
+        self.__registered_getters[get_key] = (obj_key, getter)
+
+    def map_objects(self, queue, is_blocking):
+        DEBUG=False
+        msg='Device memory objects have already been mapped to host.'
+        assert not self.__mapped, msg
+        evt = None
+        for (obj_key, (dev_buf, flags)) in self.__registered_objects.iteritems():
+            if DEBUG:
+                msg='Mapping {}...'.format(obj_key.full_tag)
+                print msg
+            if is_blocking:
+                host_buf = dev_buf.map_to_host(queue=queue, is_blocking=is_blocking, flags=flags)
+            else:
+                host_buf, evt = dev_buf.map_to_host(queue=queue, is_blocking=is_blocking, flags=flags)
+            self.__mapped_objects[obj_key] = host_buf
+        for (get_key, (obj_key, getter)) in self.__registered_getters.iteritems():
+            if DEBUG:
+                msg='Applying getter {} to mapped buffer {}...'.format(get_key.full_tag, obj_key.full_tag)
+                print msg
+            self.__mapped_objects[get_key] = getter(self.__mapped_objects[obj_key])
+        self.__mapped = True
+        return evt
+
+    def unmap_objects(self):
+        msg='Device memory objects have already been unmapped from host.'
+        assert self.__mapped, msg
+        self.__mapped_objects.clear()
+        self.__mapped = False
+
+    def get_mapped_object(self, key):
+        msg='Device memory objects have not been mapped to host yet.'
+        assert self.__mapped, msg
+        msg='Device memory object "{}" has not been mapped.'
+        msg=msg.format(key)
+        assert key in self.__mapped_objects, msg
+        return self.__mapped_objects[key]
+
+    def build_object_getter(self, key):
+        msg='Device memory object "{}" has not been registered.'
+        msg=msg.format(key)
+        assert key in self.__registered_objects, msg
+        return functools.partial(self.get_mapped_object, key=key)
+   
+    @contextmanager
+    def map_objects_to_host(self, queue=None, is_blocking=True):
+        if self.__registered_objects:
+            assert (self.cl_env is not None)
+            queue = first_not_None(queue, self.cl_env.default_queue)
+            try:
+                evt = self.map_objects(queue, is_blocking)
+                yield self.OpenClMappedMemoryObjectGetter(self, evt)
+            except:
+                raise
+            finally:
+                self.unmap_objects()
+        else:
+            try:
+                yield
+            except:
+                raise
+
diff --git a/hysop/backend/host/python/operator/curl.py b/hysop/backend/host/python/operator/curl.py
new file mode 100644
index 0000000000000000000000000000000000000000..25aa3a3e70d87dde42a4240981296903452a4d32
--- /dev/null
+++ b/hysop/backend/host/python/operator/curl.py
@@ -0,0 +1,121 @@
+
+import functools
+import numba as nb
+from hysop import __DEFAULT_NUMBA_TARGET__
+from hysop.tools.types import check_instance, first_not_None
+from hysop.tools.decorators import debug
+from hysop.tools.numpywrappers import npw
+from hysop.backend.host.host_operator import HostOperator
+from hysop.core.graph.graph import op_apply
+from hysop.operator.base.curl import SpectralCurlOperatorBase
+
+@nb.guvectorize([
+    nb.void(nb.float32[:,::-1],    nb.float32[::-1],    nb.float32[:,::-1]), 
+    nb.void(nb.complex64[:,::-1],  nb.float32[::-1],    nb.complex64[:,::-1]), 
+    nb.void(nb.complex64[:,::-1],  nb.complex64[::-1],  nb.complex64[:,::-1]), 
+    nb.void(nb.float64[:,::-1],    nb.float64[::-1],    nb.float64[:,::-1]), 
+    nb.void(nb.complex128[:,::-1], nb.float64[::-1],    nb.complex128[:,::-1]), 
+    nb.void(nb.complex128[:,::-1], nb.complex128[::-1], nb.complex128[:,::-1]), 
+    ],  '(n,m),(m)->(n,m)', 
+    target=__DEFAULT_NUMBA_TARGET__, nopython=True, cache=True)
+def filter_curl_2d__0(Fin, K, Fout):
+    for i in range(0, Fin.shape[0]):
+        for j in range(0, Fin.shape[1]):
+            Fout[i,j] = +K[j]*Fin[i,j]
+
+@nb.guvectorize([
+    nb.void(nb.float32[:,::-1],    nb.float32[::-1],    nb.float32[:,::-1]), 
+    nb.void(nb.complex64[:,::-1],  nb.float32[::-1],    nb.complex64[:,::-1]), 
+    nb.void(nb.complex64[:,::-1],  nb.complex64[::-1],  nb.complex64[:,::-1]), 
+    nb.void(nb.float64[:,::-1],    nb.float64[::-1],    nb.float64[:,::-1]), 
+    nb.void(nb.complex128[:,::-1], nb.float64[::-1],    nb.complex128[:,::-1]), 
+    nb.void(nb.complex128[:,::-1], nb.complex128[::-1], nb.complex128[:,::-1]), 
+    ],  '(n,m),(m)->(n,m)', 
+    target=__DEFAULT_NUMBA_TARGET__, nopython=True, cache=True)
+def filter_curl_2d__1(Fin, K, Fout):
+    for i in range(0, Fin.shape[0]):
+        for j in range(0, Fin.shape[1]):
+            Fout[i,j] = -K[j]*Fin[i,j]
+
+
+@nb.guvectorize([
+    nb.void(nb.float32[:,:,::-1],    nb.float32[::-1],    nb.float32[:,:,::-1]), 
+    nb.void(nb.complex64[:,:,::-1],  nb.float32[::-1],    nb.complex64[:,:,::-1]), 
+    nb.void(nb.complex64[:,:,::-1],  nb.complex64[::-1],  nb.complex64[:,:,::-1]), 
+    nb.void(nb.float64[:,:,::-1],    nb.float64[::-1],    nb.float64[:,:,::-1]), 
+    nb.void(nb.complex128[:,:,::-1], nb.float64[::-1],    nb.complex128[:,:,::-1]), 
+    nb.void(nb.complex128[:,:,::-1], nb.complex128[::-1], nb.complex128[:,:,::-1]), 
+    ],  '(n,m,p),(p)->(n,m,p)', 
+    target=__DEFAULT_NUMBA_TARGET__, nopython=True, cache=True)
+def filter_curl_3d__0(Fin, K, Fout):
+    for i in range(0, Fin.shape[0]):
+        for j in range(0, Fin.shape[1]):
+            for k in range(0, Fin.shape[2]):
+                Fout[i,j,k] = +K[k]*Fin[i,j,k]
+
+@nb.guvectorize([
+    nb.void(nb.float32[:,:,::-1],    nb.float32[::-1],    nb.float32[:,:,::-1]), 
+    nb.void(nb.complex64[:,:,::-1],  nb.float32[::-1],    nb.complex64[:,:,::-1]), 
+    nb.void(nb.complex64[:,:,::-1],  nb.complex64[::-1],  nb.complex64[:,:,::-1]), 
+    nb.void(nb.float64[:,:,::-1],    nb.float64[::-1],    nb.float64[:,:,::-1]), 
+    nb.void(nb.complex128[:,:,::-1], nb.float64[::-1],    nb.complex128[:,:,::-1]), 
+    nb.void(nb.complex128[:,:,::-1], nb.complex128[::-1], nb.complex128[:,:,::-1]), 
+    ],  '(n,m,p),(p)->(n,m,p)', 
+    target=__DEFAULT_NUMBA_TARGET__, nopython=True, cache=True)
+def filter_curl_3d__1(Fin, K, Fout):
+    for i in range(0, Fin.shape[0]):
+        for j in range(0, Fin.shape[1]):
+            for k in range(0, Fin.shape[2]):
+                Fout[i,j,k] = -K[k]*Fin[i,j,k]
+
+class PythonSpectralCurl(SpectralCurlOperatorBase, HostOperator):
+    """
+    Compute the curl by using an python FFT backend.
+    """
+        
+        
+    def setup(self, work):
+        super(PythonSpectralCurl, self).setup(work=work)
+        
+        dim = self.dim
+        Fin, Fout = self.Fin, self.Fout
+        K, FIN, FOUT = self.dK, self.FIN, self.FOUT
+
+        msg='Unsupported number of components {}.'
+        if (dim==2):
+            if (Fin.nb_components in (1,2)):
+                curl_F0 = functools.partial(filter_curl_2d__0, FIN[0], K[0], FOUT[0])
+                curl_F1 = functools.partial(filter_curl_2d__1, FIN[1], K[1], FOUT[1])
+                curl_filters = (curl_F0, curl_F1)
+            else:
+                raise ValueError(msg.format(Fin.nb_components))
+        elif (dim==3):
+            if (Fin.nb_components == 3):
+                assert (Fout.nb_components == 3), Fout.nb_components
+                curl_filters = ()
+                for i in xrange(3):
+                    curl_Fi = functools.partial(filter_curl_3d__0, FIN[i], K[i], FOUT[i])
+                    curl_filters += (curl_Fi,)
+                for i in xrange(3):
+                    curl_Fi = functools.partial(filter_curl_3d__1, FIN[3+i], K[3+i], FOUT[3+i])
+                    curl_filters += (curl_Fi,)
+            else:
+                raise ValueError(msg.format(Fin.nb_components))
+        else:
+            msg='Unsupported dimension {}.'.format(dim)
+            raise ValueError(msg)
+        assert len(self.forward_transforms) == len(self.backward_transforms) == len(curl_filters)
+        self.curl_filters = curl_filters
+
+    @op_apply
+    def apply(self, simulation=None, **kwds):
+        """Apply analytic formula."""
+        super(PythonSpectralCurl, self).apply(**kwds)
+        for (Ft, curl_filter, Bt) in zip(self.forward_transforms, 
+                                         self.curl_filters,
+                                         self.backward_transforms):
+            Ft()
+            curl_filter()
+            Bt()
+        self.dFout.exchange_ghosts()
+
diff --git a/hysop/backend/host/python/operator/derivative.py b/hysop/backend/host/python/operator/derivative.py
index 806b9f9e041076e1f64c1e0110b1339e94eebdf0..7317aa43d5dbdf8ceacbfe38e228a9a9879a7a51 100644
--- a/hysop/backend/host/python/operator/derivative.py
+++ b/hysop/backend/host/python/operator/derivative.py
@@ -1,23 +1,69 @@
 
 
-from hysop.operator.base.derivative import SpaceDerivativeBase
+from hysop.operator.base.derivative import FiniteDifferencesSpaceDerivativeBase, \
+                                           SpectralSpaceDerivativeBase
 from hysop.backend.host.host_operator import HostOperator
 from hysop.tools.decorators import debug
 from hysop.core.graph.graph import op_apply
-from hysop.numerics.stencil.stencil_generator import StencilGenerator, CenteredStencilGenerator, MPQ
+from hysop.numerics.stencil.stencil_generator import StencilGenerator, \
+        CenteredStencilGenerator, MPQ
 
-class PythonSpaceDerivative(SpaceDerivativeBase, HostOperator):
+class PythonSpectralSpaceDerivative(SpectralSpaceDerivativeBase, HostOperator):
     """
-    Applies a derivative on a field in a given direction
-    using finite differences and numpy.
+    Compute a derivative of a scalar field in a given direction
+    using spectral methods.
+    """
+    
+    def setup(self, work):
+        super(PythonSpectralSpaceDerivative, self).setup(work=work)
+        dA = self.dA
+        if self.scale_by_field:
+            assert isinstance(self.scaling_view, int)
+            aview = dA.compute_slices
+            self.scale = dA.sbuffer[self.scaling_view][aview]
+        else:
+            self.scale = dA
+    
+    @op_apply
+    def apply(self, **kwds):
+        self.Ft()
+        self.compute_derivative()
+        self.Bt()
+        self.scale_derivative()
+        self.dFout.exchange_ghosts()
+
+    def compute_derivative(self):
+        from hysop.constants import BoxBoundaryCondition
+        for nd_dkd in self.nd_dkds:
+            self.Ft.full_output_buffer[...] *= nd_dkd
+
+    def scale_derivative(self):
+        out   = self.Bt.output_buffer
+        scale = self.scale
+        if self.scale_by_field:
+            out[...] *= scale
+        elif self.scale_by_parameter:
+            if (self.scaling_view is not None):
+                out[...] *= scale[self.scaling_view]
+            else:
+                out[...] *= scale()
+        elif self.scale_by_value:
+            out[...] *= scale
+
+
+
+class PythonFiniteDifferencesSpaceDerivative(FiniteDifferencesSpaceDerivativeBase, HostOperator):
+    """
+    Compute a derivative of a scalar field in a given direction
+    using explicit finite differences.
     """
 
     @debug
     def __init__(self, **kwds):
         """
-        Initialize a SpaceDerivative operator on the python backend.
+        Initialize a FiniteDifferencesSpaceDerivative operator on the python backend.
 
-        See hysop.operator.base.derivative.SpaceDerivativeBase for 
+        See hysop.operator.base.derivative.FiniteDifferencesSpaceDerivativeBase for 
         more information.
 
         Parameters
@@ -25,15 +71,16 @@ class PythonSpaceDerivative(SpaceDerivativeBase, HostOperator):
         kwds: dict, optional
             Base class arguments.
         """
-        super(PythonSpaceDerivative, self).__init__(**kwds)
+        super(PythonFiniteDifferencesSpaceDerivative, self).__init__(**kwds)
+        assert (self.direction is not None)
         self.d = self.Fin.dim -1 - self.direction
 
     def handle_method(self, method):
-        super(PythonSpaceDerivative, self).handle_method(method)
+        super(PythonFiniteDifferencesSpaceDerivative, self).handle_method(method)
         csg = CenteredStencilGenerator()
         csg.configure(dtype=MPQ, dim=1)
         stencil = csg.generate_exact_stencil(
-                derivative=self.derivative, 
+                derivative=self.directional_derivative, 
                 order=self.space_discretization)
         self.stencil = stencil
 
@@ -44,7 +91,8 @@ class PythonSpaceDerivative(SpaceDerivativeBase, HostOperator):
         d = self.d
         
         # set min_ghosts for input field
-        requirements = super(PythonSpaceDerivative, self).get_field_requirements()
+        requirements = super(PythonFiniteDifferencesSpaceDerivative, 
+                self).get_field_requirements()
         for is_input, (field, td, req) in requirements.iter_requirements():
             if (field is self.Fin):
                 ghosts = req.min_ghosts.copy()
@@ -54,7 +102,7 @@ class PythonSpaceDerivative(SpaceDerivativeBase, HostOperator):
     
     @debug
     def discretize(self):
-        super(PythonSpaceDerivative, self).discretize()
+        super(PythonFiniteDifferencesSpaceDerivative, self).discretize()
         d = self.d
         stencil = self.stencil
         assert self.dFin.has_unique_attribute('space_step')
@@ -63,11 +111,11 @@ class PythonSpaceDerivative(SpaceDerivativeBase, HostOperator):
         assert not stencil.is_symbolic()
 
     def setup(self, work):
-        super(PythonSpaceDerivative, self).setup(work=work)
+        super(PythonFiniteDifferencesSpaceDerivative, self).setup(work=work)
         dFin, dFout, dA = self.dFin, self.dFout, self.dA
 
-        iview    = dFin.compute_slices
-        oview    = dFout.compute_slices
+        iview = dFin.compute_slices
+        oview = dFout.compute_slices
 
         self._in = dFin.sbuffer
         self.out = dFout.sbuffer[oview]
@@ -83,7 +131,7 @@ class PythonSpaceDerivative(SpaceDerivativeBase, HostOperator):
     @op_apply
     def apply(self, **kwds):
         """Compute derivative."""
-        super(PythonSpaceDerivative, self).apply(**kwds)
+        super(PythonFiniteDifferencesSpaceDerivative, self).apply(**kwds)
         stencil = self.stencil
         _in, out, scale = self._in, self.out, self.scale
         iview, d = self.iview, self.d
@@ -106,4 +154,3 @@ class PythonSpaceDerivative(SpaceDerivativeBase, HostOperator):
         elif self.scale_by_value:
             out[...] *= scale
 
-        self.dFout.exchange_ghosts()
diff --git a/hysop/backend/host/python/operator/diffusion.py b/hysop/backend/host/python/operator/diffusion.py
index ccbea71824aa07dabf576f93494b243ea413fde9..26ef4fd6601d7258aa466a0a87ffee71f26f169a 100644
--- a/hysop/backend/host/python/operator/diffusion.py
+++ b/hysop/backend/host/python/operator/diffusion.py
@@ -1,36 +1,102 @@
+import functools
+import numba as nb
 
+from hysop import __DEFAULT_NUMBA_TARGET__
+from hysop.constants import Backend
 from hysop.tools.types import check_instance, first_not_None
 from hysop.tools.decorators import debug
 from hysop.tools.numpywrappers import npw
-from hysop.backend.host.host_operator import HostOperator
+from hysop.tools.numerics import is_complex, complex_to_float_dtype
+from hysop.tools.numba_utils import make_numba_signature
+from hysop.backend.host.host_operator import HostOperator, OpenClMappable
 from hysop.core.graph.graph import op_apply
 from hysop.fields.continuous_field import Field
 from hysop.parameters.parameter import Parameter
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
-from hysop.operator.base.diffusion import DiffusionBase
+from hysop.operator.base.diffusion import DiffusionOperatorBase
 
-class PythonDiffusion(DiffusionBase, HostOperator):
+
+class PythonDiffusion(DiffusionOperatorBase, OpenClMappable, HostOperator):
     """
-    Solves the poisson equation using numpy fft.
+    Solves the implicit diffusion equation using numpy fft.
     """
+        
+    @classmethod
+    def build_diffusion_filter(cls, dim, *args, **kwds):
+        target = kwds.get('target', __DEFAULT_NUMBA_TARGET__)
+        assert len(args) == 1 + dim
+        dtype = args[0].dtype
+        if is_complex(dtype):
+            dtype = complex_to_float_dtype(dtype)
+        signature, _ = make_numba_signature(*(args+(dtype,args[0])))
+        if (dim==1):
+            @nb.guvectorize([signature],
+                '(n),(n),()->(n)', target=target,
+                nopython=True, cache=True)
+            def filter_diffusion_1d(Fin, K0, nu_dt, Fout):
+                for i in range(0, Fin.shape[0]):
+                    Fout[i] /= (1 - nu_dt*K0[i])
+            F = filter_diffusion_1d
+        elif (dim==2):
+            @nb.guvectorize([signature],
+                '(n,m),(n),(m),()->(n,m)', target=target,
+                nopython=True, cache=True)
+            def filter_diffusion_2d(Fin, K0, K1, nu_dt, Fout):
+                for i in range(Fin.shape[0]):
+                    for j in range(Fin.shape[1]):
+                        Fout[i,j] /= (1 - nu_dt*(K0[i] + K1[j]))
+            F = filter_diffusion_2d
+        elif (dim==3):
+            @nb.guvectorize([signature],
+                '(n,m,p),(n),(m),(p),()->(n,m,p)', target=target,
+                nopython=True, cache=True)
+            def filter_diffusion_3d(Fin, K0, K1, K2, nu_dt, Fout):
+                for i in range(Fin.shape[0]):
+                    for j in range(Fin.shape[1]):
+                        for k in range(Fin.shape[2]):
+                            Fout[i,j,k] /= (1 - nu_dt*(K0[i] + K1[j] + K2[k]))
+            F = filter_diffusion_3d
+        elif (dim==4):
+            @nb.guvectorize([signature],
+                '(n,m,p,q),(n),(m),(p),(q),()->(n,m,p,q)', target=target,
+                nopython=True, cache=True)
+            def filter_diffusion_4d(Fin, K0, K1, K2, K3, nu_dt, Fout):
+                for i in range(Fin.shape[0]):
+                    for j in range(Fin.shape[1]):
+                        for k in range(Fin.shape[2]):
+                            for l in range(Fin.shape[3]):
+                                Fout[i,j,k,l] /= (1 - nu_dt*(K0[i] + K1[j] + K2[k] + K3[l]))
+        else:
+            msg='{}D Diffusion filter has not been vectorized yet.'.format(dim)
+            raise NotImplementedError(msg)
+        return functools.partial(F, *args)
+
     
-    def generate_wave_numbers(self, dim, resolution, length, dtype):
-        k = npw.ix_(*tuple(2*npw.pi*1j*npw.fft.fftfreq(Ni,Li)*Ni for (Ni,Li) 
-                        in zip(resolution,length)))
-        k2 = tuple(ki*ki for ki in k)
-        self.K2 = sum(k2).real.astype(dtype)
+    def setup(self, work):
+        super(PythonDiffusion, self).setup(work=work)
+        diffusion_filters = ()
+        for (Fo,Ft,Kd) in zip(self.dFout.dfields, self.forward_transforms, self.all_dkds): 
+            args = (Ft.full_output_buffer,) + tuple(Kd)
+            F = self.build_diffusion_filter(Fo.dim, *args)
+            diffusion_filters += (F,)
+        self.diffusion_filters = diffusion_filters
+   
 
     @op_apply
     def apply(self, **kwds):
-        """Solve the diffusion equation."""
+        """Solve the implicit diffusion equation."""
         super(PythonDiffusion, self).apply(**kwds)
-        dt = self.dt()
-        nu = self.viscosity()
-        K2 = self.K2
-        
-        for (Fin,Fout) in zip(self.input_buffers, self.output_buffers):
-            Fhat = npw.fft.fftn(Fin)
-            Fhat[...] *= 1.0/(1.0-nu*dt*K2)
-            Fout[...] = npw.fft.ifftn(Fhat).real
 
-        self.dFout.exchange_ghosts()
+        nu_dt = self.nu() * self.dt()
+
+        with self.map_objects_to_host():
+            for (Ft,Bt,filter_diffusion) in zip(self.forward_transforms,
+                                                self.backward_transforms,
+                                                self.diffusion_filters):
+                Ft()
+                filter_diffusion(nu_dt, Ft.output_buffer)
+                Bt()
+
+        for Fo in self.dFout.dfields:
+            Fo.exchange_ghosts()
+
diff --git a/hysop/backend/host/python/operator/min_max.py b/hysop/backend/host/python/operator/min_max.py
index 0d56cf653a0a2066668f3a2dcb5ef173d02d2765..0f7ce42cad1887b1fabd0e0cc68bcee6a55ec1c9 100644
--- a/hysop/backend/host/python/operator/min_max.py
+++ b/hysop/backend/host/python/operator/min_max.py
@@ -3,9 +3,11 @@ from hysop.core.graph.graph import op_apply
 from hysop.operator.base.min_max import MinMaxFieldStatisticsBase, \
                                         MinMaxDerivativeStatisticsBase
 from hysop.backend.host.host_operator import HostOperator
-from hysop.backend.host.python.operator.derivative import PythonSpaceDerivative
+from hysop.backend.host.python.operator.derivative import PythonSpectralSpaceDerivative, \
+        PythonFiniteDifferencesSpaceDerivative
 
-class PythonMinMaxFieldStatistics(MinMaxFieldStatisticsBase, HostOperator):
+class PythonMinMaxFieldStatistics(MinMaxFieldStatisticsBase, 
+                                  HostOperator):
     """Python implementation backend of operator MinMaxFieldStatistics."""
 
     @debug
@@ -20,17 +22,21 @@ class PythonMinMaxFieldStatistics(MinMaxFieldStatisticsBase, HostOperator):
         self.compute_statistics(**kwds)
 
 
-class PythonMinMaxDerivativeStatistics(MinMaxDerivativeStatisticsBase, PythonSpaceDerivative):
-    """Python implementation backend of operator MinMaxDerivativeStatistics."""
+class PythonMinMaxSpectralDerivativeStatistics(MinMaxDerivativeStatisticsBase, 
+                                               PythonSpectralSpaceDerivative):
+    """Python implementation backend of operator MinMaxSpectralDerivativeStatistics."""
+    @op_apply
+    def apply(self, **kwds):
+        """Compute derivative and then statistics."""
+        super(PythonMinMaxSpectralDerivativeStatistics, self).apply(**kwds)
+        self.compute_statistics(**kwds)
 
-    @debug
-    def __init__(self, **kwds):
-        """See MinMaxDerivativeStatisticsBase.__init__()."""
-        super(PythonMinMaxDerivativeStatistics, self).__init__(**kwds)
 
+class PythonMinMaxFiniteDifferencesDerivativeStatistics(MinMaxDerivativeStatisticsBase, 
+                                                        PythonFiniteDifferencesSpaceDerivative):
+    """Python implementation backend of operator MinMaxFiniteDifferencesDerivativeStatistics."""
     @op_apply
     def apply(self, **kwds):
-        """Compute derivative and than statistics."""
-        super(PythonMinMaxDerivativeStatistics, self).apply(**kwds)
+        """Compute derivative and then statistics."""
+        super(PythonMinMaxFiniteDifferencesDerivativeStatistics, self).apply(**kwds)
         self.compute_statistics(**kwds)
-
diff --git a/hysop/backend/host/python/operator/penalization.py b/hysop/backend/host/python/operator/penalization.py
index ba84eb8826e789cbb5a6ffa25318b89ff18bd49c..6f3d0d58a7e0faf7655954babd42fc2dea255bfa 100755
--- a/hysop/backend/host/python/operator/penalization.py
+++ b/hysop/backend/host/python/operator/penalization.py
@@ -45,7 +45,7 @@ class PythonPenalizeVorticity(HostOperator):
         check_instance(dt, ScalarParameter)
         check_instance(coeff, (ScalarParameter, float))
         check_instance(obstacles, (tuple, dict), values=Field,
-                       keys=(ScalarParameter, float))
+                       keys=(ScalarParameter, float), check_kwds=False)
 
         input_fields = {velocity: variables[velocity],
                         vorticity: variables[vorticity]}
diff --git a/hysop/backend/host/python/operator/poisson.py b/hysop/backend/host/python/operator/poisson.py
index 09ec7ca18ff814d19d192e9644d72e85a151bb76..2658e7a930427a4e42d800fcccf11e6b068cadde 100644
--- a/hysop/backend/host/python/operator/poisson.py
+++ b/hysop/backend/host/python/operator/poisson.py
@@ -1,39 +1,115 @@
 
+import functools
+import numba as nb
+
+from hysop import __DEFAULT_NUMBA_TARGET__
 from hysop.tools.types import check_instance, first_not_None
 from hysop.tools.decorators import debug
 from hysop.tools.numpywrappers import npw
-from hysop.backend.host.host_operator import HostOperator
+from hysop.tools.numba_utils import make_numba_signature
+from hysop.backend.host.host_operator import HostOperator, OpenClMappable
 from hysop.core.graph.graph import op_apply
 from hysop.fields.continuous_field import Field
 from hysop.parameters.parameter import Parameter
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
 from hysop.operator.base.poisson import PoissonOperatorBase
 
-class PythonPoisson(PoissonOperatorBase, HostOperator):
+class PythonPoisson(PoissonOperatorBase, OpenClMappable, HostOperator):
     """
-    Solves the poisson equation using numpy fftw.
+    Solves the poisson equation using one of the host python fft backend (fftw, numpy or scipy).
     """
+            
+    @classmethod
+    def build_poisson_filter(cls, dim, *args, **kwds):
+        target = kwds.get('target', __DEFAULT_NUMBA_TARGET__)
+        assert len(args) == 2 + dim
+        signature, _ = make_numba_signature(*args)
+        if (dim==1):
+            @nb.guvectorize([signature],
+                '(n),(n)->(n)', target=target,
+                nopython=True, cache=True)
+            def filter_poisson_1d(Fin, K0, Fout):
+                Fout[0] = 0.0
+                for i in range(1, Fin.shape[0]):
+                    Fout[i] /= K0[i]
+            F = filter_poisson_1d
+        elif (dim==2):
+            @nb.guvectorize([signature],
+                '(n,m),(n),(m)->(n,m)', target=target,
+                nopython=True, cache=True)
+            def filter_poisson_2d(Fin, K0, K1, Fout):
+                for i in range(1, Fin.shape[0]):
+                    for j in range(0, Fin.shape[1]):
+                        Fout[i,j] /= (K0[i] + K1[j])
+                for j in range(1, Fin.shape[1]):
+                    Fout[0,j] /= (K0[0] + K1[j])
+                Fout[0,0] = 0
+            F = filter_poisson_2d
+        elif (dim==3):
+            @nb.guvectorize([signature],
+                '(n,m,p),(n),(m),(p)->(n,m,p)', target=target,
+                nopython=True, cache=True)
+            def filter_poisson_3d(Fin, K0, K1, K2, Fout):
+                for i in range(1, Fin.shape[0]):
+                    for j in range(0, Fin.shape[1]):
+                        for k in range(0, Fin.shape[2]):
+                            Fout[i,j,k] /= (K0[i] + K1[j] + K2[k])
+                for j in range(1, Fin.shape[1]):
+                    for k in range(0, Fin.shape[2]):
+                        Fout[0,j,k] /= (K0[0] + K1[j] + K2[k])
+                for k in range(1, Fin.shape[2]):
+                    Fout[0,0,k] /= (K0[0] + K1[0] + K2[k])
+                Fout[0,0,0] = 0.0
+            F = filter_poisson_3d
+        elif (dim==4):
+            @nb.guvectorize([signature],
+                '(n,m,p,q),(n),(m),(p),(q)->(n,m,p,q)', target=target,
+                nopython=True, cache=True)
+            def filter_poisson_4d(Fin, K0, K1, K2, K3, Fout):
+                for i in range(1, Fin.shape[0]):
+                    for j in range(0, Fin.shape[1]):
+                        for k in range(0, Fin.shape[2]):
+                            for l in range(0, Fin.shape[3]):
+                                Fout[i,j,k,l] /= (K0[i] + K1[j] + K2[k] + K3[l])
+                for j in range(1, Fin.shape[1]):
+                    for k in range(0, Fin.shape[2]):
+                        for l in range(0, Fin.shape[3]):
+                            Fout[0,j,k,l] /= (K0[0] + K1[j] + K2[k] + K3[l])
+                for k in range(1, Fin.shape[2]):
+                    for l in range(0, Fin.shape[3]):
+                        Fout[0,0,k,l] /= (K0[0] + K1[0] + K2[k] + K3[l])
+                for l in range(1, Fin.shape[3]):
+                    Fout[0,0,0,l] /= (K0[0] + K1[0] + K2[0] + K3[l])
+                Fout[0,0,0,0] = 0.0
+            F = filter_poisson_4d
+        else:
+            msg='{}D Poisson filter has not been vectorized yet.'.format(dim)
+            raise NotImplementedError(msg)
+        return functools.partial(F, *args)
+           
+    def setup(self, work):
+        super(PythonPoisson, self).setup(work=work)
+        poisson_filters = ()
+        for (Fo,Ft,Kd) in zip(self.dFout.dfields, self.forward_transforms, self.all_dkds): 
+            args = (Ft.full_output_buffer,) + tuple(Kd) + (Ft.full_output_buffer,)
+            F = self.build_poisson_filter(Fo.dim, *args)
+            poisson_filters += (F,)
+        self.poisson_filters = poisson_filters
+        
     
-    def generate_wave_numbers(self, dim, resolution, length, dtype, axes):
-        k = npw.ix_(*tuple(2*npw.pi*1j*npw.fft.fftfreq(Ni,Li)*Ni for (Ni,Li) in zip(resolution,length)))
-        k2 = (ki*ki for ki in k)
-        K2 = sum(k2).real
-        Z   = (K2==0)
-        iK2 = npw.empty_like(K2)
-        iK2[~Z] = 1.0/K2[~Z]
-        iK2[Z]  = 0.0
-        self.iK2 = iK2
-
     @op_apply
     def apply(self, **kwds):
         """Solve the Poisson equation."""
         super(PythonPoisson, self).apply(**kwds)
-        in_buffers, out_buffers = self.in_buffers, self.out_buffers
-        iK2 = self.iK2
-
-        for (din, dout) in zip(in_buffers, out_buffers):
-            dhat = npw.fft.fftn(din)
-            dhat[...] *= iK2
-            dout[...] = npw.fft.ifftn(dhat).real
-        self.dFout.exchange_ghosts()
+        
+        with self.map_objects_to_host():
+            for (Ft,Bt,filter_poisson) in zip(self.forward_transforms,
+                                              self.backward_transforms,
+                                              self.poisson_filters):
+                Ft()
+                filter_poisson()
+                Bt()
+        
+        for Fo in self.dFout.dfields:
+            Fo.exchange_ghosts()
 
diff --git a/hysop/backend/host/python/operator/poisson_curl.py b/hysop/backend/host/python/operator/poisson_curl.py
new file mode 100644
index 0000000000000000000000000000000000000000..132a4a2d3222e27f0f760bdb4dca5ed010d9752a
--- /dev/null
+++ b/hysop/backend/host/python/operator/poisson_curl.py
@@ -0,0 +1,223 @@
+
+import functools
+import numba as nb
+from hysop import __DEFAULT_NUMBA_TARGET__
+from hysop.tools.types import check_instance, first_not_None
+from hysop.tools.decorators import debug
+from hysop.tools.numpywrappers import npw
+from hysop.tools.numba_utils import make_numba_signature
+from hysop.core.graph.graph import op_apply
+from hysop.backend.host.host_operator import HostOperator, OpenClMappable
+from hysop.operator.base.poisson_curl import SpectralPoissonCurlOperatorBase
+        
+from hysop.backend.host.python.operator.diffusion import PythonDiffusion
+from hysop.backend.host.python.operator.poisson import PythonPoisson
+from hysop.backend.host.python.operator.solenoidal_projection import PythonSolenoidalProjection
+
+
+class PythonPoissonCurl(SpectralPoissonCurlOperatorBase, OpenClMappable, HostOperator):
+    """
+    Solves the poisson rotational equation using numpy fftw.
+    """
+        
+    @classmethod
+    def build_filter_curl_2d__0_m(cls, FIN, K, FOUT, target=__DEFAULT_NUMBA_TARGET__):
+        args=(FIN,K,FOUT)
+        signature, _ = make_numba_signature(*args)
+        layout = '(n,m),(m)->(n,m)'
+        @nb.guvectorize([signature], layout,
+            target=target, nopython=True, cache=True)
+        def filter_curl_2d__0_m(Fin, K1, Fout):
+            for i in range(0, Fin.shape[0]):
+                for j in range(0, Fin.shape[1]):
+                    Fout[i,j] = -K1[j]*Fin[i,j]
+        return functools.partial(filter_curl_2d__0_m, *args)
+
+
+    @classmethod
+    def build_filter_curl_2d__1_n(cls, FIN, K, FOUT, target=__DEFAULT_NUMBA_TARGET__):
+        args=(FIN,K,FOUT)
+        signature, _ = make_numba_signature(*args)
+        layout = '(n,m),(n)->(n,m)'
+        @nb.guvectorize([signature], layout,
+            target=target, nopython=True, cache=True)
+        def filter_curl_2d__1_n(Fin, K0, Fout):
+            for i in range(0, Fin.shape[0]):
+                for j in range(0, Fin.shape[1]):
+                    Fout[i,j] = +K0[i]*Fin[i,j]
+        return functools.partial(filter_curl_2d__1_n, *args)
+
+    @classmethod
+    def build_filter_curl_3d__0_n(cls, FIN, K, FOUT, target=__DEFAULT_NUMBA_TARGET__):
+        args=(FIN,K,FOUT)
+        signature, _ = make_numba_signature(*args)
+        layout='(n,m,p),(n)->(n,m,p)' 
+        @nb.guvectorize([signature], layout,
+            target=target, nopython=True, cache=True)
+        def filter_curl_3d__0_n(Fin, K, Fout):
+            for i in range(0, Fin.shape[0]):
+                for j in range(0, Fin.shape[1]):
+                    for k in range(0, Fin.shape[2]):
+                        Fout[i,j,k] = -K[i]*Fin[i,j,k]
+        return functools.partial(filter_curl_3d__0_n, *args)
+
+    @classmethod
+    def build_filter_curl_3d__0_m(cls, FIN, K, FOUT, target=__DEFAULT_NUMBA_TARGET__):
+        args=(FIN,K,FOUT)
+        signature, _ = make_numba_signature(*args)
+        layout='(n,m,p),(m)->(n,m,p)'
+        @nb.guvectorize([signature], layout,
+            target=target, nopython=True, cache=True)
+        def filter_curl_3d__0_m(Fin, K, Fout):
+            for i in range(0, Fin.shape[0]):
+                for j in range(0, Fin.shape[1]):
+                    for k in range(0, Fin.shape[2]):
+                        Fout[i,j,k] = -K[j]*Fin[i,j,k]
+        return functools.partial(filter_curl_3d__0_m, *args)
+
+    @classmethod
+    def build_filter_curl_3d__0_p(cls, FIN, K, FOUT, target=__DEFAULT_NUMBA_TARGET__):
+        args=(FIN,K,FOUT)
+        signature, _ = make_numba_signature(*args)
+        layout='(n,m,p),(p)->(n,m,p)'
+        @nb.guvectorize([signature], layout,
+            target=target, nopython=True, cache=True)
+        def filter_curl_3d__0_p(Fin, K, Fout):
+            for i in range(0, Fin.shape[0]):
+                for j in range(0, Fin.shape[1]):
+                    for k in range(0, Fin.shape[2]):
+                        Fout[i,j,k] = -K[k]*Fin[i,j,k]
+        return functools.partial(filter_curl_3d__0_p, *args)
+
+    @classmethod
+    def build_filter_curl_3d__1_n(cls, FIN, K, FOUT, target=__DEFAULT_NUMBA_TARGET__):
+        args=(FIN,K,FOUT)
+        signature, _ = make_numba_signature(*args)
+        layout='(n,m,p),(n)->(n,m,p)' 
+        @nb.guvectorize([signature], layout,
+            target=target, nopython=True, cache=True)
+        def filter_curl_3d__1_n(Fin, K, Fout):
+            for i in range(0, Fin.shape[0]):
+                for j in range(0, Fin.shape[1]):
+                    for k in range(0, Fin.shape[2]):
+                        Fout[i,j,k] += K[i]*Fin[i,j,k]
+        return functools.partial(filter_curl_3d__1_n, *args)
+
+    @classmethod
+    def build_filter_curl_3d__1_m(cls, FIN, K, FOUT, target=__DEFAULT_NUMBA_TARGET__):
+        args=(FIN,K,FOUT)
+        signature, _ = make_numba_signature(*args)
+        layout='(n,m,p),(m)->(n,m,p)' 
+        @nb.guvectorize([signature], layout,
+            target=target, nopython=True, cache=True)
+        def filter_curl_3d__1_m(Fin, K, Fout):
+            for i in range(0, Fin.shape[0]):
+                for j in range(0, Fin.shape[1]):
+                    for k in range(0, Fin.shape[2]):
+                        Fout[i,j,k] += K[j]*Fin[i,j,k]
+        return functools.partial(filter_curl_3d__1_m, *args)
+
+    @classmethod
+    def build_filter_curl_3d__1_p(cls, FIN, K, FOUT, target=__DEFAULT_NUMBA_TARGET__):
+        args=(FIN,K,FOUT)
+        signature, _ = make_numba_signature(*args)
+        layout='(n,m,p),(p)->(n,m,p)' 
+        @nb.guvectorize([signature], layout,
+            target=target, nopython=True, cache=True)
+        def filter_curl_3d__1_p(Fin, K, Fout):
+            for i in range(0, Fin.shape[0]):
+                for j in range(0, Fin.shape[1]):
+                    for k in range(0, Fin.shape[2]):
+                        Fout[i,j,k] += K[k]*Fin[i,j,k]
+        return functools.partial(filter_curl_3d__1_p, *args)
+        
+    def setup(self, work):
+        super(PythonPoissonCurl, self).setup(work=work)
+        
+        dim = self.dim
+        WIN, UIN, UOUT = self.WIN, self.UIN, self.UOUT
+        K, KK = self.K, self.KK
+        UK = self.UK
+        assert len(WIN)==len(KK), (len(WIN),len(KK))
+        assert len(UIN)==len(UOUT)==len(UK), (len(UIN),len(UOUT),len(UK))
+        
+        # diffusion filters
+        if self.should_diffuse:
+            diffusion_filters = ()
+            for (Wi,KKi) in zip(WIN, KK):
+                F = PythonDiffusion.build_diffusion_filter(dim, *((Wi,)+KKi))
+                diffusion_filters += (F,)
+            self.diffusion_filters = diffusion_filters
+        else:
+            self.diffusion_filters = None
+
+        # projection filter
+        if self.should_project:
+            self.projection_filter = PythonSolenoidalProjection.build_projection_filter(
+                                            WIN,WIN,
+                                            sum(K, ()),
+                                            sum(KK, ()))
+        else:
+            self.projection_filter = None
+
+        # poisson filter
+        poisson_filters = ()
+        for (Wi,KKi) in zip(WIN, KK):
+            F = PythonPoisson.build_poisson_filter(dim, *((Wi,)+KKi+(Wi,)))
+            poisson_filters += (F,)
+        self.poisson_filters = poisson_filters
+        
+        # curl filter
+        if (dim==2):
+            curl0 = self.build_filter_curl_2d__0_m(UIN[0], UK[0], UOUT[0])
+            curl1 = self.build_filter_curl_2d__1_n(UIN[1], UK[1], UOUT[1])
+            curl_filters = ((curl0,),(curl1,))
+        elif (dim==3):
+            curl_filters = ()
+            filters = ((self.build_filter_curl_3d__0_n, self.build_filter_curl_3d__1_p),
+                       (self.build_filter_curl_3d__0_p, self.build_filter_curl_3d__1_m),
+                       (self.build_filter_curl_3d__0_m, self.build_filter_curl_3d__1_n))
+            for (i, (build_filter_curl_3d__0, build_filter_curl_3d__1)) in enumerate(filters):
+                j = 2*i
+                k = 2*i + 1
+                curl0 = build_filter_curl_3d__0(UIN[j], UK[j], UOUT[j])
+                curl1 = build_filter_curl_3d__1(UIN[k], UK[k], UOUT[k])
+                curl_filters += ((curl0,curl1),)
+        else:
+            msg='Unsupported dimension {}.'.format(dim)
+            raise ValueError(msg)
+        self.curl_filters = curl_filters
+
+        assert len(curl_filters)==len(self.U_backward_transforms)
+
+    @op_apply
+    def apply(self, simulation=None, **kwds):
+        """Apply analytic formula."""
+        super(PythonPoissonCurl, self).apply(**kwds)
+        
+        diffuse=self.should_diffuse
+        project=self.do_project(simu=simulation)
+
+        with self.map_objects_to_host():
+            for Ft in self.W_forward_transforms:
+                Ft()
+            if diffuse:
+                nu_dt = self.nu()*self.dt()
+                for (Wi, filter_diffusion) in zip(self.WIN, self.diffusion_filters):
+                    filter_diffusion(nu_dt, Wi)
+            if project:
+                self.projection_filter()
+            if (diffuse or project):
+                for Bt in self.W_backward_transforms:
+                    Bt()
+            for poisson_filter in self.poisson_filters:
+                poisson_filter()
+            for (curl_filters, Bt) in zip(self.curl_filters, self.U_backward_transforms):
+                for curl_filter in curl_filters:
+                    curl_filter()
+                Bt()
+
+        self.dU.exchange_ghosts()
+        if (diffuse or project):
+            self.dW.exchange_ghosts()
+
diff --git a/hysop/backend/host/python/operator/poisson_rotational.py b/hysop/backend/host/python/operator/poisson_rotational.py
deleted file mode 100644
index 5fc69544ac014d358b0eb71ea6824a4c77af288c..0000000000000000000000000000000000000000
--- a/hysop/backend/host/python/operator/poisson_rotational.py
+++ /dev/null
@@ -1,122 +0,0 @@
-
-import pyfftw, multiprocessing
-from hysop.tools.types import check_instance, first_not_None
-from hysop.tools.decorators import debug
-from hysop.tools.numpywrappers import npw
-from hysop.backend.host.host_operator import HostOperator
-from hysop.core.graph.graph import op_apply
-from hysop.operator.base.poisson_rotational import PoissonRotationalOperatorBase
-
-class PythonPoissonRotational(PoissonRotationalOperatorBase, HostOperator):
-    """
-    Solves the poisson rotational equation using numpy fftw.
-    """
-   
-    def generate_wave_numbers(self, dim, resolution, length, dtype, ctype, axes):
-        K1 = ()
-        for i in axes:
-            Ni = resolution[i]
-            Li = length[i]
-
-            if (i==dim-1):
-                k1 = 2*npw.pi*1j*npw.fft.rfftfreq(Ni,Li)*Ni
-            else:
-                k1 = 2*npw.pi*1j*npw.fft.fftfreq(Ni,Li)*Ni
-            k1 = k1.astype(dtype=ctype).copy()
-            K1 += (k1,)
-        K1 = npw.ix_(*K1[::-1])
-        K2 = sum((k1**2).real.astype(dtype=dtype).copy() for k1 in K1)
-        
-        shape = list(resolution)
-        shape[-1] = shape[-1]// 2 + 1
-        shape = tuple(shape)
-        assert npw.array_equal(K2.shape, shape)
-
-        Z   = (K2==0)
-        iK2 = npw.empty_like(K2)
-        iK2[~Z] = 1.0/K2[~Z]
-        iK2[Z]  = 0.0
-
-        FFT = pyfftw.interfaces.numpy_fft
-        nthreads = multiprocessing.cpu_count()
-
-        self.K = K1
-        self.iK2 = iK2
-        self.FFT = FFT
-        self.nthreads = nthreads
-
-    @op_apply
-    def apply(self, simulation=None, **kwds):
-        """Apply analytic formula."""
-        super(PythonPoissonRotational, self).apply(**kwds)
-        
-        if (self.dim == 2):
-            self._solve2d(**kwds)
-        elif (self.dim == 3):
-            project=self._do_project(simu=simulation)
-            self._solve3d(project=project, simulation=simulation, **kwds)
-        else:
-            msg='Unsupported dimension {}.'.format(dim)
-            raise ValueError(msg)
-
-    def _solve2d(self, **kwds):
-        W_buffers, U_buffers = self.W_buffers, self.U_buffers
-        iK2, (Ky,Kx) = self.iK2, self.K
-        FFT, nthreads = self.FFT, self.nthreads
-
-        W0,    = W_buffers
-        U0, U1 = U_buffers
-        
-        W0_hat = FFT.rfftn(W0, threads=nthreads)
-        W0_hat[...] *= iK2
-        Ui_hat = npw.empty_like(W0_hat)
-
-        Ui_hat[...] = W0_hat
-        Ui_hat[...] *= -Ky
-        U0[...] = FFT.irfftn(Ui_hat, s=U0.shape, threads=nthreads)
-
-        Ui_hat[...] = W0_hat
-        Ui_hat[...] *= +Kx
-        U1[...] = FFT.irfftn(Ui_hat, s=U1.shape, threads=nthreads)
-
-        self.dU.exchange_ghosts()
-
-    def _solve3d(self, simulation, project=False, debug_dumper=None, **kwds):
-        W_buffers, U_buffers = self.W_buffers, self.U_buffers
-        iK2, (Kz,Ky,Kx) = self.iK2, self.K
-        FFT, nthreads = self.FFT, self.nthreads
-        
-        W0,W1,W2 = W_buffers
-        U0,U1,U2 = U_buffers
-        
-        W0_hat = FFT.rfftn(W0, threads=nthreads).astype(self.ctype)
-        W1_hat = FFT.rfftn(W1, threads=nthreads).astype(self.ctype)
-        W2_hat = FFT.rfftn(W2, threads=nthreads).astype(self.ctype)
-        
-        if project:
-            assert False
-            divW = iK2*(Kx*W0_hat + Ky*W1_hat + Kz*W2_hat)
-            W0_hat[...] -= Kx*divW
-            W1_hat[...] -= Ky*divW
-            W2_hat[...] -= Kz*divW
-            W0[...] = FFT.irfftn(W0_hat, s=W0.shape, threads=nthreads)
-            W1[...] = FFT.irfftn(W1_hat, s=W1.shape, threads=nthreads)
-            W2[...] = FFT.irfftn(W2_hat, s=W2.shape, threads=nthreads)
-            self.dW.exchange_ghosts()
-        
-        W0_hat[...] *= iK2
-        W1_hat[...] *= iK2
-        W2_hat[...] *= iK2 
-        
-        Ui_hat = npw.empty_like(W0_hat)
-
-        Ui_hat[...] = (-Ky*W2_hat + Kz*W1_hat)
-        U0[...] = FFT.irfftn(Ui_hat, s=U0.shape, threads=nthreads)
-
-        Ui_hat[...] = (-Kz*W0_hat + Kx*W2_hat)
-        U1[...] = FFT.irfftn(Ui_hat, s=U1.shape, threads=nthreads)
-
-        Ui_hat[...] = (-Kx*W1_hat + Ky*W0_hat)
-        U2[...] = FFT.irfftn(Ui_hat, s=U2.shape, threads=nthreads)
-        
-        self.dU.exchange_ghosts()
diff --git a/hysop/backend/host/python/operator/solenoidal_projection.py b/hysop/backend/host/python/operator/solenoidal_projection.py
index 3d88754c622286a2bfa730985e5b0f98011ac520..c131813b96f2e2fa82378186b07fcd60fde84ff9 100644
--- a/hysop/backend/host/python/operator/solenoidal_projection.py
+++ b/hysop/backend/host/python/operator/solenoidal_projection.py
@@ -1,77 +1,133 @@
 
+import functools
+import numba as nb 
+
+from hysop import __DEFAULT_NUMBA_TARGET__
 from hysop.tools.types import check_instance, first_not_None
 from hysop.tools.decorators import debug
 from hysop.tools.numpywrappers import npw
-from hysop.backend.host.host_operator import HostOperator
+from hysop.backend.host.host_operator import HostOperator, OpenClMappable
 from hysop.core.graph.graph import op_apply
 from hysop.operator.base.solenoidal_projection import SolenoidalProjectionOperatorBase
+from hysop.tools.numba_utils import make_numba_signature
 
-class PythonSolenoidalProjection(SolenoidalProjectionOperatorBase, HostOperator):
+class PythonSolenoidalProjection(SolenoidalProjectionOperatorBase, OpenClMappable, HostOperator):
     """
     Solves solenoidal projection (project a 3d field F such that div(F)=0),
-    using numpy fft.
+    using spectral methods.
     """
-   
-    def generate_wave_numbers(self, dim, resolution, length, dtype, ctype, axes):
-        Z  = (0,)*dim
-        K = npw.ix_(*tuple(2*npw.pi*1j*npw.fft.fftfreq(Ni,Li)*Ni \
-                for (Ni,Li) in zip(resolution,length)))[::-1]
-        
-        k2 = (ki*ki for ki in K)
-        K2 = sum(k2).real.copy()
-        
-        iK2 = npw.empty_like(K2)
-        iK2[K2!=0] = 1.0/K2[K2!=0]
-        iK2[Z]     = 0.0
-        
-        self.K = K
-        self.iK2 = iK2
+    
+    @classmethod
+    def build_projection_filter(cls, FIN, FOUT, K, KK, target=__DEFAULT_NUMBA_TARGET__):
+        assert len(FIN)==len(FOUT)==3
+        assert len(K)==len(KK)==9
+        args = FIN+K+KK+FOUT
+
+        signature, _ = make_numba_signature(*args)
+        layout =  '(m,n,p),(m,n,p),(m,n,p), '
+        layout += '(m),(n),(p), (m),(n),(p), (m),(n),(p), '
+        layout += '(m),(n),(p), (m),(n),(p), (m),(n),(p) '
+        layout += '-> (m,n,p),(m,n,p),(m,n,p)'
+
+        @nb.guvectorize([signature], layout,
+            target=target, nopython=True, cache=True)
+        def filter_projection_3d(Fin0, Fin1, Fin2, 
+                                 K00, K01, K02, 
+                                 K10, K11, K12, 
+                                 K20, K21, K22, 
+                                 KK00, KK01, KK02, 
+                                 KK10, KK11, KK12, 
+                                 KK20, KK21, KK22, 
+                                 Fout0, Fout1, Fout2):
+            for i in range(0, Fin0.shape[0]):
+                for j in range(0, Fin0.shape[1]):
+                    for k in range(0, Fin0.shape[2]):
+                        F0 = Fin0[i,j,k]
+                        F1 = Fin1[i,j,k]
+                        F2 = Fin2[i,j,k]
+                        G0 = K00[i]*F0
+                        G1 = K11[j]*F1
+                        G2 = K22[k]*F2
+                        L0 = KK00[i]*F0
+                        L1 = KK11[j]*F1
+                        L2 = KK22[k]*F2
+                        if ((i!=0) or (j!=0) or (k!=0)):
+                            C0 = ((L0        + K10[i]*G1 + K20[i]*G2) / (KK00[i] + KK01[j] + KK02[k]))
+                            C1 = ((K01[j]*G0 + L1        + K21[j]*G2) / (KK10[i] + KK11[j] + KK12[k]))
+                            C2 = ((K02[k]*G0 + K12[k]*G1 + L2       ) / (KK20[i] + KK21[j] + KK22[k]))
+                        else:
+                            C0 = 0
+                            C1 = 0
+                            C2 = 0
+                        Fout0[i,j,k] = Fin0[i,j,k] - C0
+                        Fout1[i,j,k] = Fin1[i,j,k] - C1
+                        Fout2[i,j,k] = Fin2[i,j,k] - C2
+
+        return functools.partial(filter_projection_3d, *args)
+
+    def build_divergence_filters(self, target=__DEFAULT_NUMBA_TARGET__):
+        def make_div_filter(*args):
+            signature, _ = make_numba_signature(*args)
+            layout =  '(m,n,p), (m,n,p), (m,n,p), (m),(n),(p) -> (m,n,p)'
+
+            @nb.guvectorize([signature], layout,
+                target=target, nopython=True, cache=True)
+            def compute_div_3d(Fin0, Fin1, Fin2, K0, K1, K2, Fout):
+                for i in range(0, Fin0.shape[0]):
+                    for j in range(0, Fin0.shape[1]):
+                        for k in range(0, Fin0.shape[2]):
+                            Fout[i,j,k] = (K0[i]*Fin0[i,j,k] + K1[j]*Fin1[i,j,k] + K2[k]*Fin2[i,j,k])
+
+            return functools.partial(compute_div_3d, *args)
+
+        if self.compute_divFin:
+            FIN, K, DIV_IN = self.FIN, self.K, self.DIV_IN
+            K = (K[0], K[4], K[8])
+            args = FIN+K+DIV_IN
+            self.pre_filter_div = make_div_filter(*args)
+        else:
+            self.pre_filter_div = None
+
+        if self.compute_divFout:
+            FOUT, K, DIV_OUT = self.FOUT, self.K, self.DIV_OUT
+            K = (K[0], K[4], K[8])
+            args = FOUT+K+DIV_OUT
+            self.post_filter_div = make_div_filter(*args)
+        else:
+            self.post_filter_div = None
+    
+    @debug
+    def setup(self, work):
+        super(PythonSolenoidalProjection, self).setup(work=work)
+        FIN, FOUT = self.FIN, self.FOUT
+        K, KK = self.K, self.KK
+        self.filter_projection = self.build_projection_filter(FIN, FOUT, K, KK)
+        self.build_divergence_filters()
 
     @op_apply
     def apply(self, simulation=None, **kwds):
         super(PythonSolenoidalProjection, self).apply(**kwds)
-        self._solve3d()
-
-    def _solve3d(self, **kwds):
-        Fin_buffers, Fout_buffers = self.Fin_buffers, self.Fout_buffers
-        divFin_buffers, divFout_buffers = self.divFin_buffers, self.divFout_buffers
-        iK2, K = self.iK2, self.K
-
-        IN0,IN1,IN2    = Fin_buffers
-        OUT0,OUT1,OUT2 = Fout_buffers
-        DIV_IN,  = divFin_buffers
-        DIV_OUT, = divFout_buffers
-        
-        IN0_hat = npw.fft.fftn(IN0)
-        IN1_hat = npw.fft.fftn(IN1)
-        IN2_hat = npw.fft.fftn(IN2)
-
-        in0 = IN0_hat[0,0,0]
-        in1 = IN1_hat[0,0,0]
-        in2 = IN2_hat[0,0,0]
-
-        div = (K[0]*IN0_hat + K[1]*IN1_hat + K[2]*IN2_hat)
-        if (DIV_IN is not None):
-            DIV_IN[...] = npw.fft.ifftn(div).real
-            self.ddivFin.exchange_ghosts()
-        div *= iK2
-
-        IN0_hat -= K[0]*div
-        IN1_hat -= K[1]*div
-        IN2_hat -= K[2]*div
-
-        IN0_hat[0,0,0] = in0
-        IN1_hat[0,0,0] = in1
-        IN2_hat[0,0,0] = in2
-        
-        if (DIV_OUT is not None):
-            div = (K[0]*IN0_hat + K[1]*IN1_hat + K[2]*IN2_hat)
-            DIV_OUT[...] = npw.fft.ifftn(div).real
-            self.ddivFout.exchange_ghosts()
 
-        OUT0[...] = npw.fft.ifftn(IN0_hat).real
-        OUT1[...] = npw.fft.ifftn(IN1_hat).real
-        OUT2[...] = npw.fft.ifftn(IN2_hat).real
+        with self.map_objects_to_host():
+            for Ft in self.forward_transforms:
+                Ft()
+
+            if self.compute_divFin:
+                self.pre_filter_div()
+                self.backward_divFin_transform()
+            
+            self.filter_projection()
 
-        self.dFout.exchange_ghosts()
+            if self.compute_divFout:
+                self.post_filter_div()
+                self.backward_divFout_transform()
 
+            for Bt in self.backward_transforms:
+                Bt()
+
+        if self.compute_divFin:
+            self.ddivFin.exchange_ghosts()
+        if self.compute_divFout:
+            self.ddivFout.exchange_ghosts()
+        for Bt in self.backward_transforms:
+            Bt.dfield.exchange_ghosts()
diff --git a/hysop/constants.py.in b/hysop/constants.py.in
index 3fdc576a5d5f8662cebd6ed01f731319dce47b1f..00ad5a1f0384ac3c7f0e61966249c50a7b36796d 100644
--- a/hysop/constants.py.in
+++ b/hysop/constants.py.in
@@ -102,6 +102,9 @@ GhostMask = EnumFactory.create('GhostMask',
 ExchangeMethod = EnumFactory.create('ExchangeMethod',
         ['ISEND_IRECV', 'NEIGHBOR_ALL_TO_ALL_V', 'NEIGHBOR_ALL_TO_ALL_W'])
 
+SpectralTransformAction = EnumFactory.create('SpectralTransformAction',
+        ['OVERWRITE', 'ACCUMULATE'])
+
 Precision = EnumFactory.create('Precision',
     ['DEFAULT', 'SAME', 'QUAD', 'LONG_DOUBLE', 'DOUBLE', 'FLOAT', 'HALF'])
 """Real number precision configuration (default_precision = HYSOP_REAL)."""
@@ -134,10 +137,52 @@ DirectionLabels = 'XYZAB'
 Directions = EnumFactory.create('Directions', DirectionLabels.split())
 """Directions enum"""
 
+BoxBoundaryCondition = EnumFactory.create('BoxBoundaryCondition',
+        [ 'PERIODIC', 
+          'SYMMETRIC',  # no slip (wall-like):  Un=0 and dU||/dn = 0
+          'OUTFLOW' ])  #                       dUn/dn = 0 and U|| = 0
+"""Box boundary conditions enum."""
+
 BoundaryCondition = EnumFactory.create('BoundaryCondition',
         [ 'NONE', # => boundaries are already in ghosts
-          'PERIODIC', 'NEUMANN', 'DIRICHLET', 'MIXED' ])
+          'PERIODIC', 'MIXED',
+          'HOMOGENEOUS_NEUMANN', 'HOMOGENEOUS_DIRICHLET', 
+          'NEUMANN', 'DIRICHLET' ])
 """Boundary conditions enum"""
+        
+def boundary2str(b):
+    """Helper function to convert a BoundaryCondition to a short string."""
+    sstr = {
+        BoundaryCondition.NONE:                  'NONE',
+        BoundaryCondition.MIXED:                 'MIXED',
+        BoundaryCondition.PERIODIC:              'PER',
+        BoundaryCondition.HOMOGENEOUS_NEUMANN:   'HNEU',
+        BoundaryCondition.HOMOGENEOUS_DIRICHLET: 'HDIR',
+        BoundaryCondition.NEUMANN:               'NEU',
+        BoundaryCondition.DIRICHLET:             'DIR',
+    }
+    if b in sstr:
+        return sstr[b]
+    else:
+        return str(b)
+    
+def format_boundaries(lboundaries, rboundaries):
+    """Helper function format boundary conditions."""
+    return ','.join(('{}/{}'.format(boundary2str(lb), boundary2str(rb))
+                      for (lb,rb) in zip(lboundaries, rboundaries)))
+
+BoundaryExtension = EnumFactory.create('BoundaryExtension',
+        ['PERIODIC', 'EVEN', 'ODD'])
+
+TransformType = EnumFactory.create('Transform', 
+        ['NONE',
+         'FFT', 'IFFT', 
+         'RFFT', 'IRFFT',
+         'DCT_I', 'DCT_II', 'DCT_III', 'DCT_IV',
+         'DST_I', 'DST_II', 'DST_III', 'DST_IV',
+         'IDCT_I', 'IDCT_II', 'IDCT_III', 'IDCT_IV',
+         'IDST_I', 'IDST_II', 'IDST_III', 'IDST_IV'])
+
 
 FieldProjection = EnumFactory.create('FieldProjection',
         ['NONE', 'EVERY_STEP'])
diff --git a/hysop/core/arrays/array.py b/hysop/core/arrays/array.py
index 13efefd44d2b9c28e52b72f316689675c54031bd..f2fa434ea080844913a8b00a1236408b66709f5d 100644
--- a/hysop/core/arrays/array.py
+++ b/hysop/core/arrays/array.py
@@ -383,7 +383,9 @@ class Array(object):
              See https://docs.scipy.org/doc/numpy-1.10.0/reference/internals.html for more information
              about C versus Fortran ordering in numpy.
              """
-             return tuple(np.argsort(self.strides, kind='mergesort')[::-1].tolist())
+             strides = np.asarray(self.strides, dtype=np.int64)
+             axes = np.argsort(-strides, kind='mergesort')
+             return tuple(axes.tolist())
 
     def is_fp(self):
         """
@@ -888,11 +890,9 @@ class Array(object):
         return self._handle.__repr__()
 
     def __setitem__(self, slices, value):
-        if slices_empty(slices, self.shape):
+        if any( (s==0) for s in self[slices].shape ):
             return
         self._call('__setitem__', slices, value)
 
     def __getitem__(self, slices):
-        if slices_empty(slices, self.shape):
-            return None
         return self._call('__getitem__', slices)
diff --git a/hysop/core/arrays/array_backend.py b/hysop/core/arrays/array_backend.py
index 57d3292c3d93fd96e86921d846d1a49b1a8c8b15..29278852eacd7e1d32e5c559d1fbe1974df1c5cd 100644
--- a/hysop/core/arrays/array_backend.py
+++ b/hysop/core/arrays/array_backend.py
@@ -340,7 +340,7 @@ Exception was:
             raise RuntimeError(msg)
         else:
             shape = shapes[0]
-
+        
         if not all(order==orders[0] for order in orders):
             order=MemoryOrdering.C_CONTIGUOUS
         else:
diff --git a/hysop/core/arrays/tests/test_array.py b/hysop/core/arrays/tests/test_array.py
index b8da5d64b2a673777eea20af92d0ff957ec9df19..2520b3435bcd3d58773478a7b27cb79bfcc8f62b 100644
--- a/hysop/core/arrays/tests/test_array.py
+++ b/hysop/core/arrays/tests/test_array.py
@@ -704,8 +704,8 @@ class TestArray(object):
             float_types    = (np.float16,np.float32,np.float64, np.longdouble)
             complex_types  = (np.complex64, np.complex128, np.clongdouble)
         else:
-            signed_types   = (np.int32,)
-            unsigned_types = (np.uint32,)
+            signed_types   = ()
+            unsigned_types = ()
             float_types    = (np.float32,)
             complex_types  = (np.complex64,)
         
@@ -826,7 +826,7 @@ class TestArray(object):
 if __name__ == '__main__':
     test = TestArray()
     test.test_host_array_backend_allocator()
-    test.test_host_array_backend_mempool()
+    #test.test_host_array_backend_mempool()
     if __HAS_OPENCL_BACKEND__:
         test.test_opencl_array_backend_allocator()
-        test.test_opencl_array_backend_pool()
+        #test.test_opencl_array_backend_pool()
diff --git a/hysop/core/graph/computational_graph.py b/hysop/core/graph/computational_graph.py
index 2db7f1fadc7997ccad6c54686c5867fba3f71c08..8da3ed28705f08e8db85e30e387439bfecc8662b 100644
--- a/hysop/core/graph/computational_graph.py
+++ b/hysop/core/graph/computational_graph.py
@@ -455,6 +455,7 @@ class ComputationalGraph(ComputationalGraphNode):
                 self.candidate_input_tensors.update(node.candidate_input_tensors)
                 self.candidate_output_tensors.update(node.candidate_output_tensors)
                 nodes = node.generate()
+                assert (nodes is not None), node
                 self.push_nodes(*nodes)
             else:
                 msg = 'Given node is not an instance of ComputationalGraphNode (got a {}).'
@@ -772,12 +773,12 @@ class ComputationalGraph(ComputationalGraphNode):
 
     @debug
     @discretized
-    def setup(self, work=None):
+    def setup(self, work=None, allow_subbuffers=False):
         if self.ready:
             return
         if (work is None):
             work = self.get_work_properties()
-            work.allocate()
+            work.allocate(allow_subbuffers=allow_subbuffers)
         reduced_graph = self.reduced_graph
         operators     = reduced_graph.vertex_properties['operators']
         for vid in self.sorted_nodes:
diff --git a/hysop/core/graph/computational_operator.py b/hysop/core/graph/computational_operator.py
index a9383460071ae9962b9fa60e3f0e6d6358d8abb7..75dc455c2d1866945c4cbd548f295680f72ec4a5 100644
--- a/hysop/core/graph/computational_operator.py
+++ b/hysop/core/graph/computational_operator.py
@@ -158,39 +158,47 @@ class ComputationalGraphOperator(ComputationalGraphNode):
         is initialized.
         """
         pass
+         
+    @debug
+    def create_topology_descriptors(self):
+        """
+        Called in get_field_requirements, just after handle_method
+         Topology requirements (or descriptors) are:
+             1) min and max ghosts for each input and output variables
+             2) allowed splitting directions for cartesian topologies
+        """
+        # by default we create HOST (cpu) TopologyDescriptors
+        for (field, topo_descriptor) in self.input_fields.iteritems():
+            topo_descriptor = TopologyDescriptor.build_descriptor(
+                    backend=Backend.HOST,
+                    operator=self,
+                    field=field,
+                    handle=topo_descriptor)
+            self.input_fields[field] = topo_descriptor
+
+        for (field, topo_descriptor) in self.output_fields.iteritems():
+            topo_descriptor = TopologyDescriptor.build_descriptor(
+                    backend=Backend.HOST,
+                    operator=self,
+                    field=field,
+                    handle=topo_descriptor)
+            self.output_fields[field] = topo_descriptor
 
     @debug
     def get_field_requirements(self):
          """
          Called just after handle_method(), ie self.method has been set.
-         Topology requirements are:
-             1) min and max ghosts for each input and output variables
-             2) allowed splitting directions for cartesian topologies
          Field requirements are:
              1) required local and global transposition state, if any.
              2) required memory ordering (either C or Fortran)
          Default is Backend.HOST, no min or max ghosts, MemoryOrdering.ANY
          and no specific default transposition state for each input and output variables.
          """
+        
+         # Create the topology descriptors
+         self.create_topology_descriptors()
 
-         # by default we create HOST (cpu) TopologyDescriptors
-         for (field, topo_descriptor) in self.input_fields.iteritems():
-             topo_descriptor = TopologyDescriptor.build_descriptor(
-                     backend=Backend.HOST,
-                     operator=self,
-                     field=field,
-                     handle=topo_descriptor)
-             self.input_fields[field] = topo_descriptor
-
-         for (field, topo_descriptor) in self.output_fields.iteritems():
-             topo_descriptor = TopologyDescriptor.build_descriptor(
-                     backend=Backend.HOST,
-                     operator=self,
-                     field=field,
-                     handle=topo_descriptor)
-             self.output_fields[field] = topo_descriptor
-
-         # and we use default DiscreteFieldRequirements (ie. no min ghosts, no max ghosts,
+         # We use default DiscreteFieldRequirements (ie. no min ghosts, no max ghosts,
          # can_split set to True in all directions, all TranspositionStates
          # and C memory ordering).
          input_field_requirements  = {}
@@ -226,7 +234,6 @@ class ComputationalGraphOperator(ComputationalGraphNode):
         Called after get_field_requirements to get global operator requirements.
 
         By default we enforce unique:
-            *number of ghosts
             *transposition state
             *cartesian topology shape
             *memory order (either C or fortran)
@@ -490,10 +497,18 @@ class ComputationalGraphOperator(ComputationalGraphNode):
         computed and returned.
         """
         requests = OperatorMemoryRequests(self)
+        delayed_requests = {}
         for dfield in self.discrete_fields:
             if dfield.is_tmp:
-                req_id = 'tmp_{}_{}'.format(dfield.name, dfield.tag)
-                requests.push_mem_request(req_id, dfield.dfield.memory_request)
+                if (dfield.mem_tag is not None):
+                    req_id = dfield.mem_tag
+                    try:
+                        requests.push_mem_request(req_id, dfield.dfield.memory_request)
+                    except ValueError:
+                        pass
+                else:
+                    req_id = 'tmp_{}_{}'.format(dfield.name, dfield.tag)
+                    requests.push_mem_request(req_id, dfield.dfield.memory_request)
         return requests
 
     @debug
@@ -508,19 +523,25 @@ class ComputationalGraphOperator(ComputationalGraphNode):
 
         Automatically honour temporary field memory requests.
         """
+        self.allocate_tmp_fields(work)
         super(ComputationalGraphOperator, self).setup(work)
+
+    def allocate_tmp_fields(self, work):
         for dfield in self.discrete_fields:
-            if dfield.is_tmp:
-                req_id = 'tmp_{}_{}'.format(dfield.name, dfield.tag)
+            if dfield.is_tmp and (dfield._dfield._data is None):
+                if (dfield.mem_tag is not None):
+                    req_id = dfield.mem_tag
+                else:
+                    req_id = 'tmp_{}_{}'.format(dfield.name, dfield.tag)
                 data = work.get_buffer(self, req_id)
                 dfield.dfield.honor_memory_request(data)
-
-    def supported_backends(self):
+    
+    @classmethod
+    def supported_backends(cls):
         """
         Return the backends that this operator's topologies can support as a set.
         By default all operators support only Backend.HOST.
         """
-        # TODO check if this is really required
         return set([Backend.HOST])
 
     @debug
@@ -650,7 +671,7 @@ class ComputationalGraphOperator(ComputationalGraphNode):
                 msg+='\n -> this operator only supports the following backends:'
                 msg+='\n     *'+'\n     *'.join([str(b) for b in supported_backends])
                 msg+='\n -> bad fields were:'
-                msg+='\n     *'+'\n     *'.join([f.field_tag for f in bad_fields])
+                msg+='\n     *'+'\n     *'.join([f.full_tag for f in bad_fields])
                 print '\nFATAL ERROR: Topology backend mismatch.\n'
                 print 'Offending topologies were:'
                 for t in topologies:
@@ -678,3 +699,8 @@ class ComputationalGraphOperator(ComputationalGraphNode):
     @classmethod
     def default_method(cls):
         return dict()
+    
+    @property
+    def enable_opencl_host_buffer_mapping(self):
+        return False
+
diff --git a/hysop/core/graph/node_generator.py b/hysop/core/graph/node_generator.py
index a340663c10cfebe6e426924a4fd8f0b46cbc0079..67c26dfff3cbb9f492395e8bedcdd875135ab792 100644
--- a/hysop/core/graph/node_generator.py
+++ b/hysop/core/graph/node_generator.py
@@ -24,7 +24,7 @@ class ComputationalGraphNodeGenerator(object):
         self.nodes     = []
         self.generated = False
         self.candidate_input_tensors = set(filter(lambda x: x.is_tensor, candidate_input_tensors))
-        self.candidate_output_tensors = set(filter(lambda x: x.is_tensor, candidate_output_tensors))
+        self.candidate_output_tensors = set(filter(lambda x: x.is_tensor,candidate_output_tensors))
 
     @abstractmethod
     def _generate(self, **kargs):
@@ -66,7 +66,8 @@ class ComputationalGraphNodeGenerator(object):
                     elif isinstance(op,ComputationalGraphNodeGenerator):
                         nodes = op.generate(**kargs)
                         if (nodes is None):
-                            msg='FATAL ERROR: {}::{}.generate() returned None, this is an implementation bug.'
+                            msg='FATAL ERROR: {}::{}.generate() returned None, '
+                            msg+='this is an implementation bug.'
                             msg=msg.format(type(op), op.name)
                             raise RuntimeError(msg)
                         self.candidate_input_tensors.update(op.candidate_input_tensors)
@@ -81,8 +82,10 @@ class ComputationalGraphNodeGenerator(object):
                 raise
             assert len(self.nodes)>=1
         
-            self.candidate_input_tensors  = set(filter(lambda x: x.is_tensor, self.candidate_input_tensors))
-            self.candidate_output_tensors = set(filter(lambda x: x.is_tensor, self.candidate_output_tensors))
+            self.candidate_input_tensors  = set(filter(lambda x: x.is_tensor, 
+                self.candidate_input_tensors))
+            self.candidate_output_tensors = set(filter(lambda x: x.is_tensor, 
+                self.candidate_output_tensors))
             
             self._post_generate(**kargs)
             self.generated = True
diff --git a/hysop/core/graph/node_requirements.py b/hysop/core/graph/node_requirements.py
index e590417f113936e871a7489a4a8fe2dc39eedeac..c3cc185de2c9733755af916e32f55ed04b49f8f1 100644
--- a/hysop/core/graph/node_requirements.py
+++ b/hysop/core/graph/node_requirements.py
@@ -130,14 +130,18 @@ class OperatorRequirements(NodeRequirements):
                 assert (req.can_split is not None)
                 if (can_split is not None):
                     if npw.sum(can_split*req.can_split)==0:
-                        msg ='::GLOBAL OPERATOR REQUIREMENTS ERROR::\n'
-                        msg+='Previous cartesian split directions: \n  {} required by fields {}\nare incompatible '
-                        msg+='with cartesian split directions requirements \n  {} enforced by {} field {}.\n'
-                        msg=msg.format(can_split, field_names, 
-                                       req.can_split, 
-                                      'input' if is_input else 'output',
-                                       field.name)
-                        raise RuntimeError(msg)
+                        if all(can_split == req.can_split):
+                            pass
+                        else:
+                            msg ='::GLOBAL OPERATOR REQUIREMENTS ERROR::\n'
+                            msg+='Previous cartesian split directions: \n  {} required by fields {}\nare incompatible '
+                            msg+='with cartesian split directions requirements \n  {} enforced by {} field {}.'
+                            msg=msg.format(can_split, field_names, 
+                                           req.can_split, 
+                                          'input' if is_input else 'output',
+                                           field.name)
+                            msg+='\nDomain cannot be splitted accross multiple processes.\n'
+                            raise RuntimeError(msg)
                     else:
                         can_split *= req.can_split
                 else:
diff --git a/hysop/core/graph/tests/test_graph.py b/hysop/core/graph/tests/test_graph.py
index d7cbcd2594b87dabb7478e247256d97275a25ba9..25746378e3a27f826ae22f947aaf2553c086f734 100644
--- a/hysop/core/graph/tests/test_graph.py
+++ b/hysop/core/graph/tests/test_graph.py
@@ -1,7 +1,7 @@
 
 from hysop.domain.box        import Box
 from hysop.topology.cartesian_topology import CartesianTopology
-from hysop.tools.parameters  import Discretization
+from hysop.tools.parameters  import CartesianDiscretization
 from hysop.fields.continuous_field import Field
 from hysop.core.graph.all import ComputationalGraphOperator, ComputationalGraph
 
@@ -11,24 +11,9 @@ class _ComputationalGraph(ComputationalGraph):
         return True
     
 class _ComputationalGraphOperator(ComputationalGraphOperator):
-    def initialize(self, methpd):
-        pass
-    def discretize(self):
-        pass
-    def get_work_properties(self):
-        return None
-    def setup(self,work):
-        pass
     def apply(self):
         pass
 
-    def available_methods(self):
-        return {}
-    def default_method(self):
-        return {}
-    def handle_method(self, method):
-        pass
-    
     @classmethod
     def supports_multiple_topologies(cls):
         return True
@@ -44,10 +29,10 @@ def test_graph_build(display=False):
     rho1g = Field(domain=box, name='rho1g')
     rho1p = Field(domain=box, name='rho1p')
     
-    d3d0 = Discretization(resolution=(64,64,64), ghosts=None)
-    d3d1 = Discretization(resolution=(128,128,128), ghosts=None)
-    t0  = CartesianTopology(box,d3d0,3)
-    t1  = CartesianTopology(box,d3d1,3)
+    d3d0 = CartesianDiscretization(resolution=(64,64,64), ghosts=None, default_boundaries=True)
+    d3d1 = CartesianDiscretization(resolution=(128,128,128), ghosts=None, default_boundaries=True)
+    t0  = CartesianTopology(domain=box, discretization=d3d0)
+    t1  = CartesianTopology(domain=box, discretization=d3d1)
 
     ops = [
                 ('copyW',     [Wg],         [Wp]),
@@ -99,6 +84,5 @@ def test_graph_build(display=False):
     if display:
         g.display()
 
-
 if __name__ == '__main__':
-    test_graph_build(display=True)
+    test_graph_build(display=False)
diff --git a/hysop/core/memory/buffer.py b/hysop/core/memory/buffer.py
index e33cbf23e9feeb3ca8dfc85f962a5c978ffd0af9..9014bb58bc2f23bb8fe2ff636a10097821ee1e3b 100644
--- a/hysop/core/memory/buffer.py
+++ b/hysop/core/memory/buffer.py
@@ -16,7 +16,6 @@ class Buffer(object):
     """
     
     #/!\ ptr, size and int_ptr properties should be redefined in child classes.
-    __metaclass__ = ABCMeta
     _DEBUG=False
 
     def __init__(self, size, **kwds):
@@ -73,11 +72,6 @@ class PooledBuffer(Buffer):
     """
     Memory pool allocated buffer wrapper.
     """
-    __metaclass__ = ABCMeta
-
-    """
-    Extra flag for debugging PooledBuffers.
-    """
 
     def __init__(self, pool, buf, alloc_sz, size, alignment, **kwds):
         """
@@ -130,7 +124,7 @@ class PooledBuffer(Buffer):
     def __del__(self):
         if PooledBuffer._DEBUG:
             print 'pooled buffer __del__() (id={})'.format(id(self))
-        if (self._buf is not None):
+        if hasattr(self, '_buf') and (self._buf is not None):
             self.release()
 
     buf     = property(get_buf)
diff --git a/hysop/core/memory/memory_request.py b/hysop/core/memory/memory_request.py
index 2b656e42d899ced748a55ffbaa8013770587560a..e6c65a33aa2b712a6c051e2f29f0b9f3eab8f18d 100644
--- a/hysop/core/memory/memory_request.py
+++ b/hysop/core/memory/memory_request.py
@@ -117,6 +117,11 @@ class MemoryRequest(object):
         Real number bytes that will be allocated for all components.
         """
         return self.nb_components*(self.data_bytes_per_component() + self.alignment - 1)
+    def max_bytes_per_component(self):
+        """
+        Real number bytes that will be allocated for all components.
+        """
+        return (self.data_bytes_per_component() + self.alignment - 1)
     def min_dtype_alignment(self,dtype):
         """
         Returns the minimum alignement of the allocated buffer (in bytes).
@@ -144,7 +149,7 @@ class MemoryRequest(object):
     @classmethod
     def cartesian_dfield_like(cls, name, dfield,
             nb_components=None, initial_values=None, dtype=None,
-            global_resolution=None, ghosts=None,
+            grid_resolution=None, ghosts=None,
             backend=None, is_read_only=None):
         from hysop.fields.cartesian_discrete_field import CartesianDiscreteScalarFieldView
         check_instance(dfield,  CartesianDiscreteScalarFieldView)
@@ -159,7 +164,7 @@ class MemoryRequest(object):
 
         (dfield, request, request_id) = dfield.tmp_dfield_like(name=name, backend=backend,
                 nb_components=nb_components, initial_values=initial_values, dtype=dtype,
-                global_resolution=global_resolution, ghosts=ghosts, is_read_only=is_read_only)
+                grid_resolution=grid_resolution, ghosts=ghosts, is_read_only=is_read_only)
 
         return (dfield, request, request_id)
 
@@ -381,7 +386,7 @@ class MultipleOperatorMemoryRequests(object):
                 op_buffer_sizes = ()
                 op_reqs = ()
                 for req in requests:
-                    nbytes = req.data_bytes_per_component()
+                    nbytes = req.max_bytes_per_component()
                     for i in xrange(req.nb_components):
                         op_buffer_sizes += (nbytes,)
                         op_reqs         += (req,)
@@ -413,8 +418,17 @@ class MultipleOperatorMemoryRequests(object):
                             views[op][old_req.id] = tuple(req_views)
                         req_views = []
                     nbytes = req.data_bytes_per_component()
-                    assert buf.size >= nbytes
-                    view = buf[:nbytes].view(dtype=req.dtype).reshape(req.shape)
+                    if (backend.kind is Backend.HOST):
+                        alignment = req.alignment
+                        assert buf.size >= nbytes + alignment - 1
+                        ptr, read_only = buf.__array_interface__['data']
+                        align_offset = (-ptr % alignment)
+                    else:
+                        # no way to enforce more than device alignment for other backends (opencl)
+                        # because for OpenCL 1.x a device pointer address can change between kernel
+                        # calls.
+                        align_offset = 0
+                    view = buf[align_offset:align_offset+nbytes].view(dtype=req.dtype).reshape(req.shape)
                     req_views.append(view)
                     old_req = req
                 assert old_req.id not in views[op]
@@ -446,6 +460,7 @@ class MultipleOperatorMemoryRequests(object):
 
     def sreport(self):
         all_requests = {}
+        totals = {}
         for (backend, backend_requests) in self._all_requests_per_backend.iteritems():
             total=0
             for op in sorted(backend_requests.keys(), key=lambda op: getattr(op, 'name', None)):
@@ -461,6 +476,7 @@ class MultipleOperatorMemoryRequests(object):
                     local_total+=req.max_bytes()
                 if local_total>total:
                     total=local_total
+            totals[backend] = total
 
         if len(all_requests):
             sizes = {}
@@ -478,7 +494,12 @@ class MultipleOperatorMemoryRequests(object):
 
             ss=''
             for (backend, backend_srequests) in all_requests.iteritems():
+<<<<<<< HEAD
+                kind = backend.kind
+=======
+                total = totals[backend]
                 kind = backend.kind
+>>>>>>> master
                 if (kind == Backend.OPENCL):
                     precision = u' on device {}'.format(backend.device.name.strip())
                 else:
diff --git a/hysop/core/memory/mempool.py b/hysop/core/memory/mempool.py
index cb9a6a85f174b08242f038e8121ed867f0edfd30..75e5f62afcded9600b71e1da433de6156624814e 100644
--- a/hysop/core/memory/mempool.py
+++ b/hysop/core/memory/mempool.py
@@ -77,7 +77,9 @@ class MemoryPool(object):
 
         max_alloc_bytes = max_alloc_bytes or default_limit
         verbose = verbose if isinstance(verbose,bool) else __DEBUG__
-
+        
+        if isinstance(name, unicode):
+            name = str(name)
         check_instance(name, str)
         check_instance(mantissa_bits, int)
         check_instance(max_alloc_bytes,(int,long), allow_none=True)
diff --git a/hysop/core/mpi/tests/__init__.py b/hysop/core/mpi/tests/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/hysop/core/mpi/tests/test_bridge.py b/hysop/core/mpi/tests/test_bridge.py
deleted file mode 100755
index a83af7d447aafb36131de30102f3b457ae32a9f6..0000000000000000000000000000000000000000
--- a/hysop/core/mpi/tests/test_bridge.py
+++ /dev/null
@@ -1,120 +0,0 @@
-from hysop.domain.box import Box
-from hysop.tools.parameters import Discretization
-from hysop.core.mpi.bridge import Bridge, BridgeInter, BridgeOverlap
-from hysop.core.mpi import main_size, main_comm
-from hysop.core.mpi.tests.utils import create_subtopos, create_inter_topos
-
-
-import math
-
-
-def test_bridge2D():
-    dimension = 2
-    Lx = Ly = 2 * math.pi
-    dom = Box(length=[Lx, Ly], origin=[0., 0.])
-    # Global discretization, no ghosts
-    r_ng = Discretization([33, ] * dimension)
-    # Global discretization, with ghosts
-    r_wg = Discretization([33, ] * dimension, [0, 1])
-    source = dom.create_topology(discretization=r_ng,
-                                 cutdir=[False, True])
-    target = dom.create_topology(discretization=r_wg)
-    bridge = Bridge(source, target)
-    # We cannot really check something interesting,
-    # so we just create a bridge.
-    # The real tests are done in test_redistribute.py
-    print bridge
-
-
-def test_bridge3D():
-    dimension = 3
-    Lx = 2 * math.pi
-    dom = Box(length=[Lx, ] * dimension, origin=[0., ] * dimension)
-    # Global discretization, no ghosts
-    r_ng = Discretization([33, ] * dimension)
-    # Global discretization, with ghosts
-    r_wg = Discretization([33, ] * dimension, [0, 1, 2])
-    source = dom.create_topology(discretization=r_ng,
-                                 cutdir=[False, False, True])
-    target = dom.create_topology(discretization=r_wg)
-    bridge = Bridge(source, target)
-    # We cannot really check something interesting,
-    # so we just create a bridge.
-    # The real tests are done in test_redistribute.py
-    print bridge
-
-
-def test_bridge_overlap():
-    """
-    Try the pathologic case where source and target do not apply on
-    the same group of process but when groups overlap.
-    """
-
-    if main_size < 4:
-        return
-    dimension = 3
-    # Global discretization, no ghosts
-    r_ng = Discretization([33, ] * dimension)
-    # Global discretization, with ghosts
-    r_wg = Discretization([33, ] * dimension, [0, 1, 2])
-    Lx = 2 * math.pi
-    dom = Box(length=[Lx, ] * dimension, origin=[0., ] * dimension)
-    source_topo, target_topo = create_subtopos(dom, r_ng, r_wg)
-    bridge = BridgeOverlap(source=source_topo, target=target_topo,
-                           comm_ref=main_comm)
-    assert bridge is not None
-
-
-def test_bridgeInter2D():
-    if main_size < 4:
-        return
-
-    dimension = 2
-    # Global discretization, no ghosts
-    r_ng = Discretization([33, ] * dimension)
-    # Global discretization, with ghosts
-    r_wg = Discretization([33, ] * dimension, [0, 1])
-    dom, topo1, topo2 = create_inter_topos(2, r_ng, r_wg)
-    CPU = 1
-    GPU = 4
-    bridge = BridgeInter(topo1, main_comm, source_id=CPU, target_id=GPU)
-    tr = bridge.transfer_types()
-    assert bridge is not None
-    assert isinstance(tr, dict)
-    # We cannot really check something interesting,
-    # so we just create a bridge.
-    # The real tests are done in test_redistribute.py
-
-    # Bridge from topo2 on GPU to topo1 on CPU:
-    if dom.is_on_task(GPU):
-        bridge2 = BridgeInter(topo2, main_comm, source_id=GPU, target_id=CPU)
-    elif dom.is_on_task(CPU):
-        bridge2 = BridgeInter(topo1, main_comm, source_id=GPU, target_id=CPU)
-    assert bridge2 is not None
-
-
-def test_bridgeInter3D():
-    if main_size < 4:
-        return
-    dimension = 3
-    # Global discretization, no ghosts
-    r_ng = Discretization([33, ] * dimension)
-    # Global discretization, with ghosts
-    r_wg = Discretization([33, ] * dimension, [0, 1, 2])
-    dom, topo1, topo2 = create_inter_topos(3, r_ng, r_wg)
-    CPU = 1
-    GPU = 4
-    bridge = BridgeInter(topo1, main_comm, source_id=CPU, target_id=GPU)
-    tr = bridge.transfer_types()
-    assert bridge is not None
-    assert isinstance(tr, dict)
-    # We cannot really check something interesting,
-    # so we just create a bridge.
-    # The real tests are done in test_redistribute.py
-    # Bridge from topo2 on GPU to topo1 on CPU:
-    if dom.is_on_task(GPU):
-        bridge2 = BridgeInter(topo2, main_comm, source_id=GPU, target_id=CPU)
-    elif dom.is_on_task(CPU):
-        bridge2 = BridgeInter(topo1, main_comm, source_id=GPU, target_id=CPU)
-    assert bridge2 is not None
-
diff --git a/hysop/core/mpi/tests/test_topology.py b/hysop/core/mpi/tests/test_topology.py
deleted file mode 100755
index 21961fb679d98d18bb8d6e3d5bb936fd47055cf2..0000000000000000000000000000000000000000
--- a/hysop/core/mpi/tests/test_topology.py
+++ /dev/null
@@ -1,251 +0,0 @@
-from hysop.domain.box import Box
-from hysop.constants import HYSOP_DEFAULT_TASK_ID
-from hysop.tools.parameters import Discretization
-from hysop.core.mpi import main_size
-import numpy as np
-from hysop.tools.numpywrappers import npw
-
-
-N = 33
-r1D = Discretization([N])
-r2D = Discretization([N, 17])  # No ghosts
-r3D = Discretization([N, N, 17])  # No ghosts
-r3DGh = Discretization([N, N, 17], [2, 2, 2])  # Ghosts
-
-CPU = HYSOP_DEFAULT_TASK_ID
-GPU = 29
-proc_tasks = [CPU] * main_size
-if main_size > 2:
-    proc_tasks[-1] = GPU
-    proc_tasks[0] = GPU
-
-dom3D = Box(proc_tasks=proc_tasks)
-dom2D = Box(dimension=2, proc_tasks=proc_tasks)
-dom3D_notask = Box()
-
-# A mesh of reference for comparion.
-# Obviously we assume that default topo constructor works well...
-toporef = dom3D.create_topology(r3DGh, dim=1)
-refmesh = toporef.mesh
-toporef_notask = dom3D_notask.create_topology(r3DGh, dim=1)
-toporef2d = dom2D.create_topology(r2D, dim=1)
-refmesh2d = toporef2d.mesh
-
-
-def check2D(topo):
-    assert topo.size == main_size
-    assert topo.task_id() == HYSOP_DEFAULT_TASK_ID
-    assert np.allclose(topo.mesh.discretization.resolution,
-                       r2D.resolution)
-
-
-def check3D(topo):
-    assert topo.size == main_size
-    assert topo.task_id() == HYSOP_DEFAULT_TASK_ID
-    assert np.allclose(topo.mesh.discretization.resolution,
-                       r3D.resolution)
-
-
-# ===== 2D domains ====
-# Default:
-def test_create_default_topology_2d():
-    dom = Box(dimension=2)
-    topo = dom.create_topology(r2D)
-    assert topo.domain is dom
-    check2D(topo)
-
-
-# Test taskid
-def test_create_default_topology2_2d():
-    dom = Box(dimension=2, proc_tasks=proc_tasks)
-    topo = dom.create_topology(r2D)
-    assert topo.domain == dom
-    assert topo.size == dom2D.task_comm.Get_size()
-    if dom.is_on_task(CPU):
-        assert topo.task_id() == CPU
-    if dom.is_on_task(GPU):
-        assert topo.task_id() == GPU
-
-
-# Input : dimension
-def test_create_topologyFromDim_2d():
-    dom = Box(dimension=2)
-    topo1 = dom.create_topology(r2D, dim=1)
-    check2D(topo1)
-    topo2 = dom.create_topology(r2D, dim=2)
-    check2D(topo2)
-
-
-# Input : shape
-def test_create_topologyFromShape_2d():
-    dom = Box(dimension=2)
-    if main_size == 8:
-        topoShape = npw.asdimarray([2, 4])
-        topo = dom.create_topology(r2D, shape=topoShape)
-        assert topo.domain == dom
-        assert topo.dimension == 2
-        assert topo.size == main_size
-        assert (topo.shape == topoShape).all()
-        assert (topo.mesh.local_resolution == [16, 4]).all()
-
-    else:
-        shape = [main_size, 1]
-        topo = dom.create_topology(r2D, shape=shape)
-        assert (topo.shape == shape).all()
-        assert topo.dimension == 1
-        check2D(topo)
-
-
-# Input = cutdir
-def test_create_topologyFromCutdir_2d():
-    dom = Box(dimension=2)
-    if main_size >= 4:
-        topo = dom.create_topology(r2D, cutdir=[False, True])
-        assert topo.domain == dom
-        assert topo.dimension == 1
-        assert topo.size == main_size
-        assert (topo.shape == [1, main_size]).all()
-
-    topo2 = dom.create_topology(r2D, cutdir=[True, False])
-    assert (topo2.shape == [main_size, 1]).all()
-    assert topo2.dimension == 1
-    check2D(topo2)
-
-
-# plane topo with input mesh
-def test_create_planetopology_2d():
-    dom = Box(dimension=2)
-    offs = refmesh2d.start()
-    lres = refmesh2d.resolution
-    topo = dom.create_plane_topology_from_mesh(global_start=offs,
-                                               localres=lres,
-                                               discretization=r2D,
-                                               )
-    assert topo.domain == dom
-    assert topo.dimension == 1
-    assert topo.size == main_size
-    assert (topo.shape == [1, main_size]).all()
-    assert topo.mesh == refmesh2d
-    topo2 = dom.create_plane_topology_from_mesh(discretization=r2D,
-                                                global_start=offs,
-                                                localres=lres, cdir=0)
-    assert topo2.domain == dom
-    assert topo2.dimension == 1
-    assert topo2.size == main_size
-    assert (topo2.shape == [main_size, 1]).all()
-
-
-# ===== 3D domains ====
-# Default:
-def test_create_default_topology():
-    dom = Box()
-    topo = dom.create_topology(r3D)
-    assert topo.domain is dom
-    check3D(topo)
-
-
-# Test taskid
-def test_create_default_topology2():
-    dom = Box(proc_tasks=proc_tasks)
-    topo = dom.create_topology(r3D)
-    assert topo.domain == dom
-    assert topo.size == dom3D.task_comm.Get_size()
-    if dom.is_on_task(CPU):
-        assert topo.task_id() == CPU
-    if dom.is_on_task(GPU):
-        assert topo.task_id() == GPU
-
-
-# Input : dimension
-def test_create_topologyFromDim():
-    dom = Box()
-    topo1 = dom.create_topology(r3D, dim=1)
-    check3D(topo1)
-    topo2 = dom.create_topology(r3D, dim=2)
-    check3D(topo2)
-    topo3 = dom.create_topology(r3D, dim=3)
-    check3D(topo3)
-
-
-# Input : shape
-def test_create_topologyFromShape():
-    dom = Box()
-    if main_size == 8:
-        topoShape = npw.asdimarray([2, 2, 2])
-        topo = dom.create_topology(r3D, shape=topoShape)
-        assert topo.domain == dom
-        assert topo.dimension == 3
-        assert topo.size == main_size
-        assert (topo.shape == topoShape).all()
-        assert (topo.mesh.local_resolution == [16, 16, 8]).all()
-
-    else:
-        shape = [main_size, 1, 1]
-        topo = dom.create_topology(r3D, shape=shape)
-        assert (topo.shape == shape).all()
-        assert topo.dimension == 1
-        check3D(topo)
-
-
-# Input = cutdir
-def test_create_topologyFromCutdir():
-    dom = Box()
-    if main_size == 8:
-        topo = dom.create_topology(r3D, cutdir=[False, True, True])
-        assert topo.domain == dom
-        assert topo.dimension == 2
-        assert topo.size == main_size
-        assert (topo.shape == [1, 2, 4]).all()
-
-    topo2 = dom.create_topology(r3D, cutdir=[False, True, False])
-    assert (topo2.shape == [1, main_size, 1]).all()
-    assert topo2.dimension == 1
-    check3D(topo2)
-
-
-# plane topo with input mesh
-def test_create_planetopology():
-    dom = Box()
-    offs = refmesh.start()
-    lres = refmesh.resolution
-    topo = dom.create_plane_topology_from_mesh(discretization=r3DGh,
-                                               global_start=offs,
-                                               localres=lres)
-    assert topo.domain == dom
-    assert topo.dimension == 1
-    assert topo.size == main_size
-    assert (topo.shape == [1, 1, main_size]).all()
-    assert topo.mesh == refmesh
-    topo2 = dom.create_plane_topology_from_mesh(discretization=r3DGh,
-                                                global_start=offs,
-                                                localres=lres, cdir=1)
-    assert topo2.domain == dom
-    assert topo2.dimension == 1
-    assert topo2.size == main_size
-    assert (topo2.shape == [1, main_size, 1]).all()
-
-
-def test_operator_equal():
-    dom = Box()
-    topoDims = [main_size, 1, 1]
-    topo = dom.create_topology(r3DGh, shape=topoDims)
-    mesh = toporef_notask.mesh
-    topo2 = Box().create_plane_topology_from_mesh(
-        discretization=r3DGh, global_start=mesh.start(),
-        localres=mesh.local_resolution, cdir=2)
-    # Same as topo2 but the discretization
-    topo3 = Box().create_plane_topology_from_mesh(
-        discretization=r3D, global_start=mesh.start(),
-        localres=mesh.local_resolution, cdir=2)
-    assert topo2.mesh == mesh
-    assert (topo2.shape == toporef_notask.shape).all()
-    assert topo2.domain == toporef_notask.domain
-    assert topo2 == toporef_notask
-    assert not topo2 == topo3
-    if main_size > 1:
-        assert not topo == topo2
-    else:
-        assert topo == topo2
-
-    # test not equal ...
-    assert topo2 != topo3
diff --git a/hysop/core/mpi/tests/utils.py b/hysop/core/mpi/tests/utils.py
deleted file mode 100644
index 1823e0cb63561fa7b757614dff20c75cf3626398..0000000000000000000000000000000000000000
--- a/hysop/core/mpi/tests/utils.py
+++ /dev/null
@@ -1,168 +0,0 @@
-"""Functions used in mpi-related tests.
-"""
-
-from hysop.core.mpi import main_comm, main_rank, main_size
-from hysop.tools.parameters import MPIParams
-import hysop as pp
-GPU = 4
-CPU = 1
-OTHER = 12
-
-
-def create_multitask_context(dim, discr):
-    """Create a domain and a topology in a multi-task context
-
-    Parameters
-    ----------
-    dim : int
-       domain dimension
-    discr : :class:`~hysop.tools.parameters.Discretization
-       space discretization
-
-    Returns
-    -------
-    dom : :class:`~hysop.domain.box.Box`
-        a domain (box-shaped) defined on three differents tasks
-    topo : :class:`~hysop.topology.topology.CartesianTopology`
-        topology associated to the domain
-
-    Notes
-    -----
-
-    This function has sense only if the number of mpi processe
-    is is greater than or equal to 3. In that case, 3 tasks are defined:
-    proc 0 and last proc handle 'GPU' task, proc 1 'OTHER' task and all other
-    procs, 'CPU' task.
-    """
-    proc_tasks = [CPU, ] * main_size
-    if main_size > 2:
-        proc_tasks[-1] = GPU
-        proc_tasks[0] = GPU
-        proc_tasks[1] = OTHER
-    topodim = max(dim, 2)
-    dom = pp.Box(dimension=dim, proc_tasks=proc_tasks)
-    # Create a topology, which represents a different context
-    # on each task.
-    topo = dom.create_topology(discr, dim=topodim)
-
-    return dom, topo
-
-
-def create_subtopos(domain, discr_source, discr_target):
-    """Split main mpi-communicator into two topologies.
-
-    Parameters
-    ----------
-    dom : :class:`~hysop.domain.box.Box`
-        a domain (box-shaped)
-    discr_source, discr_target : :class:`~hysop.tools.parameters.Discretization
-       space discretizations for each sub-topo
-
-
-    Returns
-    -------
-    source_topo, target_topo: class:`~hysop.topology.topology.CartesianTopology`
-        topologies associated with rank 0 and last rank for 'target'
-        and other ranks for 'source'.
-    """
-    # split main comm into two groups
-    rk_source = [i for i in xrange(main_size)]
-    rk_target = list(rk_source)
-    rk_target.pop(-1)
-    rk_target.pop(0)
-    g_source = main_comm.group.Incl(rk_source)
-    g_target = main_comm.group.Incl(rk_target)
-    # Create the sub-communicators and the related topologies
-    comm_source = main_comm.Create(g_source)
-    mpi_source = MPIParams(comm=comm_source)
-    if main_rank in rk_source:
-        source_topo = domain.create_topology(discretization=discr_source,
-                                             mpi_params=mpi_source)
-    else:
-        source_topo = None
-
-    comm_target = main_comm.Create(g_target)
-    mpi_target = MPIParams(comm=comm_target)
-    if main_rank in rk_target:
-        target_topo = domain.create_topology(discretization=discr_target,
-                                             mpi_params=mpi_target)
-    else:
-        target_topo = None
-    return source_topo, target_topo
-
-
-def create_nonoverlap_topos(domain, discr_source, discr_target):
-    """Split main mpi-communicator into two topologies.
-
-    Parameters
-    ----------
-    dom : :class:`~hysop.domain.box.Box`
-        a domain (box-shaped)
-    discr_source, discr_target : :class:`~hysop.tools.parameters.Discretization
-       space discretizations for each sub-topo
-
-
-    Returns
-    -------
-    source_topo, target_topo: class:`~hysop.topology.topology.CartesianTopology`
-        topologies associated with even ranks for 'source'
-        and odd ranks for 'target'.
-    """
-    rk_source = [i for i in xrange(main_size) if i % 2 == 0]
-    rk_target = [i for i in xrange(main_size) if i % 2 != 0]
-    g_source = main_comm.group.Incl(rk_source)
-    g_target = main_comm.group.Incl(rk_target)
-    # Create the sub-communicators and the related topologies
-    comm_source = main_comm.Create(g_source)
-    mpi_source = MPIParams(comm=comm_source)
-    if main_rank in rk_source:
-        source_topo = domain.create_topology(discretization=discr_source,
-                                             mpi_params=mpi_source)
-    else:
-        source_topo = None
-
-    comm_target = main_comm.Create(g_target)
-    mpi_target = MPIParams(comm=comm_target)
-    if main_rank in rk_target:
-        target_topo = domain.create_topology(discretization=discr_target,
-                                             mpi_params=mpi_target)
-    else:
-        target_topo = None
-    return source_topo, target_topo
-
-
-def create_inter_topos(dim, discr1, discr2):
-    """Create a domain and two topologies in a multi-task context
-
-    Parameters
-    ----------
-    dim : int
-       domain dimension
-    discr1, discr2 : :class:`~hysop.tools.parameters.Discretization
-        space discretizations for topo1 and topo2
-
-    Returns
-    -------
-    dom : :class:`~hysop.domain.box.Box`
-        a domain (box-shaped) defined on three differents tasks
-    topo1, topo2 : :class:`~hysop.topology.topology.CartesianTopology`
-        topologies associated to the domain
-
-    Notes
-    -----
-
-    This function has sense only if the number of mpi processe
-    is is greater than or equal to 3. In that case, 2 tasks are defined:
-    proc 0 and last proc handle 'GPU' task and all other
-    procs, 'CPU' task.
-    """
-    proc_tasks = [CPU, ] * main_size
-    if main_size > 2:
-        proc_tasks[-1] = GPU
-        proc_tasks[0] = GPU
-    topodim = max(dim, 2)
-    dom = pp.Box(dimension=dim, proc_tasks=proc_tasks)
-    topo1 = dom.create_topology(discr1, dim=topodim)
-    topo2 = dom.create_topology(discr2, dim=topodim - 1)
-
-    return dom, topo1, topo2
diff --git a/hysop/core/mpi/topo_tools.py b/hysop/core/mpi/topo_tools.py
index f3e45b538c30c075892dc3477aa4f80db2ad39a8..388adee1529333cbb9a433abeca62ea69752d53e 100644
--- a/hysop/core/mpi/topo_tools.py
+++ b/hysop/core/mpi/topo_tools.py
@@ -13,7 +13,7 @@ from hysop.mesh.mesh import Mesh
 from hysop.core.mpi import MPI
 from hysop.tools.types import check_instance, to_tuple, first_not_None
 from hysop.tools.mpi_utils import dtype_to_mpi_type
-from hysop.tools.parameters import Discretization, MPIParams
+from hysop.tools.parameters import MPIParams
 from hysop.tools.misc import Utils
 from hysop.tools.decorators import debug 
 from hysop.tools.numpywrappers import npw
diff --git a/hysop/defaults.py b/hysop/defaults.py
index 7585c2ad358f32f28d88e4abccf178faf296fe84..69b4b8f4c4898ddfb21535d6a9d6c7f3ab339a25 100644
--- a/hysop/defaults.py
+++ b/hysop/defaults.py
@@ -1,6 +1,7 @@
 
 from hysop.fields.default_fields import VelocityField, VorticityField, \
                                         DensityField, ViscosityField, \
-                                        LevelSetField, PenalizationField
+                                        LevelSetField, PenalizationField, \
+                                        CurvatureField
 from hysop.parameters.default_parameters import TimeParameters, ViscosityParameter, \
         EnstrophyParameter, KineticEnergyParameter, VolumicIntegrationParameter
diff --git a/hysop/domain/box.py b/hysop/domain/box.py
index 4435eb793ce6dd919e4098fa212ebeea68c6475a..c687872010f84f2a358662781ad84e47baacb8dd 100644
--- a/hysop/domain/box.py
+++ b/hysop/domain/box.py
@@ -1,12 +1,13 @@
 """Box-shaped domains definition.
 """
-
+import warnings
 from hysop.deps import np
-from hysop.constants import BoundaryCondition, HYSOP_REAL
+from hysop.constants import BoxBoundaryCondition, HYSOP_REAL
 from hysop.domain.domain import Domain, DomainView
 from hysop.tools.decorators import debug
 from hysop.tools.numpywrappers import npw
 from hysop.tools.types import check_instance, first_not_None, to_tuple
+from hysop.tools.warning import HysopWarning
 
 class BoxView(DomainView):
     
@@ -50,7 +51,7 @@ class BoxView(DomainView):
        
     def _get_periodicity(self):
         """Numpy array mask, True is axis is periodic, else False."""
-        periodic = BoundaryCondition.PERIODIC
+        periodic = BoxBoundaryCondition.PERIODIC
         is_lperiodic = (self.__get_domain_attr('_lboundaries')==periodic)
         is_rperiodic = (self.__get_domain_attr('_rboundaries')==periodic)
         if np.logical_xor(is_lperiodic, is_rperiodic).any():
@@ -80,10 +81,13 @@ class BoxView(DomainView):
                 self.full_tag,
                 ','.join(('{:1.1f}'.format(val) for val in self.origin)),
                 ','.join(('{:1.1f}'.format(val) for val in self.length)),
-                ','.join(('{}/{}'.format(str(lb)[:3],str(rb)[:3]) for (lb,rb) in \
-                        zip(*self.boundaries))),
+                self.format_boundaries(),
                 self.current_task())
     
+    def format_boundaries(self):
+        return ','.join(('{}/{}'.format(str(lb),str(rb)) for (lb,rb) in \
+                        zip(*self.boundaries)))
+    
     length = property(_get_length)
     origin = property(_get_origin)
     end = property(_get_end)
@@ -92,6 +96,7 @@ class BoxView(DomainView):
     boundaries = property(_get_boundaries)
     periodicity = property(_get_periodicity)
 
+
 class Box(BoxView, Domain):
     """ 
     Box-shaped domain description.
@@ -112,9 +117,9 @@ class Box(BoxView, Domain):
             Position of the lowest point of the box. Default [0.0, ...]
         dim: int, optional
             Dimension of the box.
-        lboundaries: array_like of BoundaryCondition
+        lboundaries: array_like of BoxBoundaryCondition
             Left boundary conditions.
-        rboundaries: array_like of BoundaryCondition
+        rboundaries: array_like of BoxBoundaryCondition
             Right boundary conditions.
 
         Attributes
@@ -127,11 +132,11 @@ class Box(BoxView, Domain):
             Position of the lowest point of the box. 
         end: np.ndarray of HYSOP_REAL
             Position of the greatest point of the box. 
-        lboundaries: np.ndarray of BoundaryCondition
+        lboundaries: np.ndarray of BoxBoundaryCondition
             Left boundary conditions.
-        rboundaries: np.ndarray of BoundaryCondition
+        rboundaries: np.ndarray of BoxBoundaryCondition
             Right boundary conditions.
-        boundaries: tuple of np.ndarray of BoundaryCondition
+        boundaries: tuple of np.ndarray of BoxBoundaryCondition
             Left and right boundary conditions as a tuple.
         periodicity: np.ndarray of bool
             Numpy array mask, True is axis is periodic, else False.
@@ -141,9 +146,9 @@ class Box(BoxView, Domain):
         check_instance(dim, (int,long), minval=1, allow_none=True)
         check_instance(length, (np.ndarray,list,tuple), values=(int,long,float), allow_none=True)
         check_instance(origin, (np.ndarray,list,tuple), values=(int,long,float), allow_none=True)
-        check_instance(lboundaries, (np.ndarray,list,tuple), values=BoundaryCondition, 
+        check_instance(lboundaries, (np.ndarray,list,tuple), values=BoxBoundaryCondition, 
                 allow_none=True)
-        check_instance(rboundaries, (np.ndarray,list,tuple), values=BoundaryCondition, 
+        check_instance(rboundaries, (np.ndarray,list,tuple), values=BoxBoundaryCondition, 
                 allow_none=True)
 
         if (length is None) and (origin is None) and (dim is None):
@@ -164,13 +169,31 @@ class Box(BoxView, Domain):
         check_instance(origin, np.ndarray, size=dim)
         assert (length>=0.0).all(), 'length < 0'
 
-        lboundaries = npw.asarray( lboundaries or (BoundaryCondition.PERIODIC,)*dim )
-        rboundaries = npw.asarray( rboundaries or (BoundaryCondition.PERIODIC,)*dim )
+        lboundaries = npw.asarray( first_not_None(lboundaries, 
+                                                 (BoxBoundaryCondition.PERIODIC,)*dim ) )
+        rboundaries = npw.asarray( first_not_None(rboundaries, 
+                                                 (BoxBoundaryCondition.PERIODIC,)*dim ) )
+
+        assert lboundaries.size == rboundaries.size == dim
         
         for i,(lb,rb) in enumerate(zip(lboundaries,rboundaries)):
-            if (lb==BoundaryCondition.PERIODIC) ^ (rb==BoundaryCondition.PERIODIC):
-                msg='Periodic BoundaryCondition mismatch on axis {}.'.format(i)
+            if (lb==BoxBoundaryCondition.PERIODIC) ^ (rb==BoxBoundaryCondition.PERIODIC):
+                msg='FATAL ERROR: Periodic BoxBoundaryCondition mismatch on axis {}.'.format(i)
+                msg+='\nGot:'
+                msg+='\n  *lboundaries: {}'.format(lboundaries)
+                msg+='\n  *rboundaries: {}'.format(rboundaries)
                 raise ValueError(msg)
+
+        nper = npw.sum(lboundaries==BoxBoundaryCondition.PERIODIC)
+        if (nper>0) and not all(lboundaries[:nper]==BoxBoundaryCondition.PERIODIC):
+            msg='\nPeriodic boundary conditions should be on last axes (ie. Z,Y,X,...), got '
+            msg+='periodicity {}.'.format(lboundaries==BoxBoundaryCondition.PERIODIC)
+            msg+='\nAll spectral solvers (including Poisson solvers) will fail with an error.'
+            msg+='\nPlease permute axes prior to problem description.'
+            msg+='\nSpecified boundaries were:'
+            msg+='\n  *lboundaries: {}'.format(lboundaries)
+            msg+='\n  *rboundaries: {}'.format(rboundaries)
+            warnings.warn(msg, HysopWarning)
         
         # double check types, to be sure RegisteredObject will work as expected
         check_instance(dim, int)
diff --git a/hysop/fields/cartesian_discrete_field.py b/hysop/fields/cartesian_discrete_field.py
index ec7f555145196767042eb2378dcd89a9bb8656dd..77c616e8189ae855aff246d16577cdd48ebc9998 100644
--- a/hysop/fields/cartesian_discrete_field.py
+++ b/hysop/fields/cartesian_discrete_field.py
@@ -28,8 +28,9 @@ from hysop.core.mpi.topo_tools import TopoTools
 
 class CartesianDiscreteScalarFieldViewContainerI(object):
     def initialize(self, formula, vectorize=False, 
-            exchange_ghosts=True, exchange_kwds=None,
-            only_finite=True, reorder=None, quiet=False, **kwds):
+            without_ghosts=False, exchange_ghosts=True,
+            exchange_kwds=None, only_finite=True,
+            reorder=None, quiet=False, **kwds):
         """
         Initialize the cartesian field data.
 
@@ -47,6 +48,8 @@ class CartesianDiscreteScalarFieldViewContainerI(object):
             Defaults to True.
         exchange_ghosts: bool, optional, defaults to True
             Should we exchange ghosts after initialization ?
+        without_ghosts: boolean, optional, defaults to False
+            Do not initialize ghosts (only for formula init).
         exchange_kwds: dict, optional,
             Extra exchange keyword arguments passed to ghost exchange.
             Only used if exchange_ghosts is set to True.
@@ -107,14 +110,21 @@ class CartesianDiscreteScalarFieldViewContainerI(object):
                 vprint(msg)
             host_backend = self.backend.host_array_backend
             data = tuple(host_backend.empty(shape=d.shape, dtype=d.dtype) 
-                         for d in self.data)
+                         for d in self.buffers)
+        
+        if without_ghosts:
+            vdata = tuple(buf[df.compute_slices]
+                          for (buf,df) in zip(data, self.discrete_field_views()))
+        else:
+            vdata = data
 
         if from_formula:
+
             # initialize from a python method
             assert ('data'   not in kwds), 'data is a reserved keyword.'
             assert ('coords' not in kwds), 'coords is a reserved keyword.'
             coords = self.get_attributes('mesh_coords')
-            formula_kwds = dict(data=data, coords=coords)
+            formula_kwds = dict(data=vdata, coords=coords)
             formula_kwds.update(kwds)
             for kwd in reorder:
                 vals = to_list(kwds[kwd])
@@ -171,7 +181,7 @@ class CartesianDiscreteScalarFieldViewContainerI(object):
                     for (d0,d1) in zip(self.data, data)), 'Array shape was altered.'
 
         if only_finite:
-            for (i,d) in enumerate(data):
+            for (i,d) in enumerate(vdata):
                 if np.isnan(d).any():
                     msg='Initialization of {} on component {} failed, got NaNs.'
                     msg=msg.format(self.pretty_name, i)
@@ -276,11 +286,20 @@ class CartesianDiscreteScalarFieldViewContainerI(object):
         strarr = np.empty_like(data, dtype=object)
         strarr[...] = data
         if (compute is not None):
-            strarr[self.compute_slices] = compute
+            if callable(compute):
+                compute = np.vectorize(compute)
+                strarr[self.compute_slices] = compute(strarr[self.compute_slices])
+            else:
+                strarr[self.compute_slices] = compute
         if (inner_ghosts is not None):
             for lg, rg, _ in self.inner_ghost_slices:
-                strarr[lg] = inner_ghosts
-                strarr[rg] = inner_ghosts
+                if callable(inner_ghosts):
+                    inner_ghosts = np.vectorize(inner_ghosts)
+                    strarr[lg] = inner_ghosts(strarr[lg])
+                    strarr[rg] = inner_ghosts(strarr[rg])
+                else:
+                    strarr[lg] = inner_ghosts
+                    strarr[rg] = inner_ghosts
         if (outer_ghosts is not None):
             for ndir in self.all_outer_ghost_slices:
                 for directions in self.all_outer_ghost_slices[ndir]:
@@ -295,13 +314,13 @@ class CartesianDiscreteScalarFieldViewContainerI(object):
 
         _formatter = {
                 object: lambda x: '{:^6}'.format(x)[:6],
-                float:  lambda x: '{:6.2f}'.format(x)
+                float:  lambda x: '{:+6.2f}'.format(x)
         }
 
         _print_opts = dict(threshold=10000, linewidth=1000, 
                              nanstr='nan', infstr='inf', 
                              formatter={'object': lambda x: _formatter.get(type(x),
-                                                                _formatter[object])(x)})
+                                                            _formatter[object])(x)})
         _print_opts.update(print_opts)
 
         from hysop.tools.contexts import printoptions
@@ -345,8 +364,6 @@ class CartesianDiscreteScalarFieldViewContainerI(object):
         return self.has_unique_attribute('resolution')
     def has_unique_ghosts(self):
         return self.has_unique_attribute('ghosts')
-    def has_unique_boundaries(self):
-        return self.has_unique_attribute('boundaries')
     def has_unique_space_step(self):
         return self.has_unique_attribute('space_step')
     def has_unique_coords(self):
@@ -367,6 +384,26 @@ class CartesianDiscreteScalarFieldViewContainerI(object):
         return self.has_unique_attribute('tstate')   
     def has_unique_memory_order(self):
         return self.has_unique_attribute('memory_order')
+    def has_unique_local_boundaries(self):
+        return self.has_unique_attribute('local_boundaries')
+    def has_unique_local_lboundaries(self):
+        return self.has_unique_attribute('local_lboundaries')
+    def has_unique_local_rboundaries(self):
+        return self.has_unique_attribute('local_rboundaries')
+    def has_unique_global_boundaries(self):
+        return self.has_unique_attribute('global_boundaries')
+    def has_unique_global_lboundaries(self):
+        return self.has_unique_attribute('global_lboundaries')
+    def has_unique_global_rboundaries(self):
+        return self.has_unique_attribute('global_rboundaries')
+    def has_unique_is_at_boundary(self):
+        return self.has_unique_attribute('is_at_boundary')
+    def has_unique_is_at_left_boundary(self):
+        return self.has_unique_attribute('is_at_left_boundary')
+    def has_unique_is_at_right_boundary(self):
+        return self.has_unique_attribute('is_at_right_boundary')
+    def has_unique_periodicity(self):
+        return self.has_unique_attribute('periodicity')
     
     @property
     def compute_resolution(self):
@@ -378,9 +415,6 @@ class CartesianDiscreteScalarFieldViewContainerI(object):
     def ghosts(self):
         return self.get_unique_attribute('ghosts')
     @property
-    def boundaries(self):
-        return self.get_unique_attribute('boundaries')
-    @property
     def space_step(self):
         return self.get_unique_attribute('space_step')
     @property
@@ -416,7 +450,37 @@ class CartesianDiscreteScalarFieldViewContainerI(object):
     @property
     def memory_order(self):
         return self.get_unique_attribute('memory_order')
-    
+    @property
+    def local_boundaries(self):
+        return self.get_unique_attribute('local_boundaries')
+    @property
+    def local_lboundaries(self):
+        return self.get_unique_attribute('local_lboundaries')
+    @property
+    def local_rboundaries(self):
+        return self.get_unique_attribute('local_rboundaries')
+    @property
+    def global_boundaries(self):
+        return self.get_unique_attribute('global_boundaries')
+    @property
+    def global_lboundaries(self):
+        return self.get_unique_attribute('global_lboundaries')
+    @property
+    def global_rboundaries(self):
+        return self.get_unique_attribute('global_rboundaries')
+    @property
+    def is_at_boundary(self):
+        return self.get_unique_attribute('is_at_boundary')
+    @property
+    def is_at_left_boundary(self):
+        return self.get_unique_attribute('is_at_left_boundary')
+    @property
+    def is_at_right_boundary(self):
+        return self.get_unique_attribute('is_at_right_boundary')
+    @property
+    def periodicity(self):
+        return self.get_unique_attribute('periodicity')
+
 
 class CartesianDiscreteScalarFieldView(CartesianDiscreteScalarFieldViewContainerI, DiscreteScalarFieldView):
     """
@@ -448,24 +512,33 @@ class CartesianDiscreteScalarFieldView(CartesianDiscreteScalarFieldViewContainer
         obj._data_view = None
         return obj
 
-    def _compute_data_view(self):
+    def _compute_data_view(self, data=None):
         """
         Compute transposed views of underlying discrete field data
         according to topology state.
+        
         This is called after the discrete field has allocated data.
         Arrays are reshaped and set read-only if necessary.
+
+        This can also be called from an hysop.backend.host.host_operator.OpenClMappable object
+        to map an opencl generated pointer to host (in this case custom data is passed 
+        and self_data == False).
         """
-        if (self._dfield._data is None):
-            msg='{}::{} data has not been set yet.'
+        self_data = (data is None)
+        data = first_not_None(data, self._dfield._data)
+        if (data is None):
+            if self_data:
+                msg='{}::{} internal data has not been set yet.'
+            else:
+                msg='{}::{} cannot compute data view from external None data.'
             msg=msg.format(type(self._dfield).__name__, self._dfield.name)
             raise RuntimeError(msg)
-
         
         if (self.memory_order is MemoryOrdering.C_CONTIGUOUS):
-            dataview = self._dfield._data.reshape(self.resolution)
+            dataview = data.reshape(self.resolution)
             assert dataview.flags.c_contiguous
         elif (self.memory_order is MemoryOrdering.F_CONTIGUOUS):
-            dataview = self._dfield._data.reshape(self.resolution[::-1])
+            dataview = data.reshape(self.resolution[::-1])
             dataview = dataview.T
             assert dataview.flags.f_contiguous
         else:
@@ -477,7 +550,11 @@ class CartesianDiscreteScalarFieldView(CartesianDiscreteScalarFieldViewContainer
         if self.is_read_only:
             if isinstance(dataview, np.ndarray):
                 npw.set_readonly(dataview)
-        self._data_view = dataview
+
+        if self_data:
+            self._data_view = dataview
+        else:
+            return dataview
 
     def __get_data_view(self):
         if (self._data_view is None):
@@ -645,14 +722,69 @@ class CartesianDiscreteScalarFieldView(CartesianDiscreteScalarFieldViewContainer
     def _get_is_tmp(self):
         """Is this DiscreteScalarField temporary ?"""
         return self._dfield.is_tmp
-
-    def _get_boundaries(self):
+    def _get_mem_tag(self):
+        return self._dfield.mem_tag
+    
+    def _get_global_lboundaries(self):
+        """Return global left boundaries."""
+        return self.mesh.global_lboundaries
+    def _get_global_rboundaries(self):
+        """Return global right boundaries."""
+        return self.mesh.global_rboundaries
+    def _get_global_boundaries(self):
+        """Return global boundaries as a tuple of left and right boundaries."""
+        return self.mesh.global_boundaries
+
+    def _get_local_lboundaries(self):
+        """
+        Return local left boundaries.
+        Boundaries on the interior of the global domain have value BoundaryCondition.NONE.
+        """
+        return self.mesh.local_lboundaries
+    def _get_local_rboundaries(self):
+        """
+        Return local right boundaries.
+        Boundaries on the interior of the global domain have value BoundaryCondition.NONE.
+        """
+        return self.mesh.local_rboundaries
+    def _get_local_boundaries(self):
         """
         Return local boundaries as a tuple of left and right boundaries.
         Boundaries on the interior of the global domain have value BoundaryCondition.NONE.
         """
         return self.mesh.local_boundaries
 
+    def _get_periodicity(self):
+        """
+        Get periodicity of the global boundaries.
+        This is not to be confused with the cartesian communicator periodicity.
+        """
+        return self.mesh.periodicity
+    
+    def _get_is_at_left_boundary(self):
+        """
+        Return a numpy boolean mask to identify processes that are on the left of the domain.
+        ie. is_at_left_boundary[d] = True means that process cartesian coordinates is the first
+        on direction d:  topology.proc_coords[d] == 0.
+        """
+        return self.mesh.is_at_left_boundary
+    def _get_is_at_right_boundary(self):
+        """
+        Return a numpy boolean mask to identify processes that are on the right of the domain.
+        ie. is_at_right_boundary[d] = True means that process cartesian coordinates
+            is the lastest on direction d:  topology.proc_coords[d] == topology.proc_shape[d] - 1.
+        """
+        return self.mesh.is_at_right_boundary
+    def _get_is_at_boundary(self):
+        """
+        Return a numpy boolean mask to identify processes that are on either on the left or on
+        the right of the domain. Processes can be on the left and the right at the same time on
+        direction d if and only if topology.proc_shape[d] == 1.
+        ie. is_at_boundary[d] = True means that process cartesian coordinates  is the first or
+            the lastest on direction d:(proc_coords[d] in [0, proc_shape[d] - 1]).
+        """
+        return self.mesh.is_at_boundary
+
     def get_outer_ghost_slices(self, *args, **kwds):
         """
         Return a tuple of tuples of slices indexing the local outer ghosts.
@@ -766,7 +898,8 @@ class CartesianDiscreteScalarFieldView(CartesianDiscreteScalarFieldViewContainer
         return s
 
 
-    def clone(self, name=None, pretty_name=None, tstate=None):
+    def clone(self, name=None, pretty_name=None, 
+                    var_name=None, latex_name=None, tstate=None):
         """
         Create a new temporary DiscreteScalarField and allocate it 
         like the current object, possibly on a different backend. 
@@ -779,14 +912,20 @@ class CartesianDiscreteScalarFieldView(CartesianDiscreteScalarFieldViewContainer
         default_name='{}__{}'.format(self.name, self._dfield._clone_id)
         default_pname='{}__{}'.format(self.pretty_name, 
                 subscript(self._dfield._clone_id).encode('utf-8'))
+        default_vname='{}__{}'.format(self.var_name, self._dfield._clone_id)
+        default_lname='{}__{}'.format(self.latex_name, self._dfield._clone_id)
         self._dfield._clone_id += 1
 
         tstate = first_not_None(tstate, self.topology_state)
-        name = first_not_None(name, default_name)
-        pretty_name = first_not_None(pretty_name, default_pname)
+        pretty_name = first_not_None(pretty_name, name, default_pname)
+        var_name    = first_not_None(var_name, name, default_vname)
+        latex_name  = first_not_None(latex_name, name, default_lname)
+        name        = first_not_None(name, default_name)
         
         dfield = CartesianDiscreteScalarField(name=name, 
                 pretty_name=pretty_name,
+                latex_name=latex_name, 
+                var_name=var_name,
                 field=self._dfield._field, 
                 topology=self._dfield._topology, 
                 init_topology_state=tstate,
@@ -796,15 +935,18 @@ class CartesianDiscreteScalarFieldView(CartesianDiscreteScalarFieldViewContainer
         return dfield
 
     def tmp_dfield_like(self, name, pretty_name=None,
+            var_name=None, latex_name=None,
             backend=None, is_read_only=None,
             initial_values=None, dtype=None,
-            global_resolution=None, ghosts=None, tstate=None, 
+            grid_resolution=None, ghosts=None, tstate=None, 
+            lboundaries=None, rboundaries=None,
             register_discrete_field=False, **kwds):
         """
         Create a new Field and a new temporary CartesianDiscreteScalarField.
         like the current object, possibly on a different backend. 
         /!\ The returned discrete field is not allocated.
         """
+        assert ('global_resolution' not in kwds), 'Specify grid_resolution instead.'
         tstate = first_not_None(tstate, self._topology_state)
         if (is_read_only is not None):
             tstate._is_read_only = is_read_only
@@ -813,11 +955,14 @@ class CartesianDiscreteScalarFieldView(CartesianDiscreteScalarFieldViewContainer
         btopo  = self._dfield._topology
 
         field = bfield.field_like(name=name, pretty_name=pretty_name,
+                                   latex_name=latex_name, var_name=var_name,
                                    initial_values=initial_values, dtype=dtype,
+                                   lboundaries=lboundaries, rboundaries=rboundaries,
                                    register_object=register_discrete_field)
 
         topology = btopo.topology_like(backend=backend,
-                global_resolution=global_resolution, ghosts=ghosts)
+                grid_resolution=grid_resolution, ghosts=ghosts,
+                lboundaries=lboundaries, rboundaries=rboundaries)
 
         dfield = TmpCartesianDiscreteScalarField(field=field, topology=topology, 
                                             init_topology_state=tstate, **kwds)
@@ -1064,12 +1209,26 @@ class CartesianDiscreteScalarFieldView(CartesianDiscreteScalarFieldViewContainer
     resolution         = property(_get_resolution)
     npoints            = property(_get_npoints)
     ghosts             = property(_get_ghosts)
-    boundaries         = property(_get_boundaries)
     space_step         = property(_get_space_step)
     is_tmp             = property(_get_is_tmp)
+    mem_tag            = property(_get_mem_tag)
     coords             = property(_get_coords)
     mesh_coords        = property(_get_mesh_coords)
+    
+    local_boundaries  = property(_get_local_boundaries)
+    local_lboundaries = property(_get_local_lboundaries)
+    local_rboundaries = property(_get_local_rboundaries)
+
+    global_boundaries   = property(_get_global_boundaries)
+    global_lboundaries  = property(_get_global_lboundaries)
+    global_rboundaries  = property(_get_global_rboundaries)
+
+    is_at_boundary       = property(_get_is_at_boundary)
+    is_at_left_boundary  = property(_get_is_at_left_boundary)
+    is_at_right_boundary = property(_get_is_at_right_boundary)
 
+    periodicity = property(_get_periodicity)
+    
     compute_slices = property(_get_compute_slices)
     inner_ghost_slices = property(get_inner_ghost_slices)
     outer_ghost_slices = property(get_outer_ghost_slices)
@@ -1127,13 +1286,11 @@ class CartesianDiscreteScalarField(CartesianDiscreteScalarFieldView, DiscreteSca
             The resolution of this field, excluding ghosts.
         ghosts: tuple
             The number of ghosts contained in this field.
-
+        
         shape: tuple
             Alias for compute_resolution.
-
         data: tuple of :class:`hysop.core.arrays.array.Array`
             Actual n-dimensional arrays of data (immutable), one per component.
-
         buffers: tuple of buffers, numpy.ndarray or pyopencl.buffers
             Return Array's data buffers.
             buffers are the lower level representation of data without any 
@@ -1180,8 +1337,9 @@ class CartesianDiscreteScalarField(CartesianDiscreteScalarFieldView, DiscreteSca
             from hysop.core.memory.memory_request import MemoryRequest
             memory_request = MemoryRequest(backend=obj.backend, 
                     dtype=obj.dtype, shape=obj.resolution)
-            obj._memory_request = memory_request
+            obj._memory_request    = memory_request
             obj._memory_request_id = obj.name
+            obj._mem_tag = field.mem_tag
         return obj
 
     def _handle_data(self, data):
@@ -1197,9 +1355,10 @@ class CartesianDiscreteScalarField(CartesianDiscreteScalarFieldView, DiscreteSca
         if (vd is not None): 
             data[self.mesh.local_compute_slices] = vd
         if (vg is not None): 
-            for (ls,rs,_) in self.mesh.local_outer_ghost_slices:
-                data[ls] = vg
-                data[rs] = vg
+            for (ls,rs,shape) in self.mesh.local_outer_ghost_slices:
+                if (shape is not None):
+                    data[ls] = vg
+                    data[rs] = vg
         if self.topology_state.is_read_only:
             npw.set_readonly(data[i])
 
@@ -1213,13 +1372,23 @@ class CartesianDiscreteScalarField(CartesianDiscreteScalarFieldView, DiscreteSca
     @property
     def is_tmp(self):
         return False
+    
+    @property
+    def mem_tag(self):
+        return self._field.mem_tag
+
+    def __eq__(self, other):
+        return id(self) == id(other)
+    def __hash__(self):
+        return id(self)
 
 
 class TmpCartesianDiscreteScalarField(CartesianDiscreteScalarField):
     @debug
     def __new__(cls, **kwds):
-        return super(TmpCartesianDiscreteScalarField, cls).__new__(cls, allocate_data=False,
+        obj = super(TmpCartesianDiscreteScalarField, cls).__new__(cls, allocate_data=False,
                 register_discrete_field=False, **kwds)
+        return obj
 
     @debug
     def __init__(self, **kwds):
diff --git a/hysop/fields/continuous_field.py b/hysop/fields/continuous_field.py
index e512271745870d3714ef3221d9f0fba94c4ff770..8b5eeca9d91589b80665cc0a822e09762145280d 100644
--- a/hysop/fields/continuous_field.py
+++ b/hysop/fields/continuous_field.py
@@ -7,154 +7,23 @@ Continuous fields description and containers.
 """
 
 import textwrap
+import sympy as sm
 from abc import ABCMeta, abstractmethod
 
-from hysop.constants         import HYSOP_REAL, HYSOP_BOOL
+from hysop.constants         import HYSOP_REAL, HYSOP_BOOL, BoundaryCondition
 from hysop.tools.decorators  import debug
 from hysop.tools.types       import check_instance, first_not_None, to_tuple
 from hysop.tools.warning    import HysopWarning
 from hysop.tools.handle import TaggedObject
 from hysop.tools.numpywrappers import npw
 from hysop.domain.domain import Domain
+from hysop.domain.box import BoxBoundaryCondition
 from hysop.topology.topology import Topology, TopologyState
-
-class SymbolContainerI(object):
-    __metaclass__ = ABCMeta
-    
-    def _get_symbol(self):
-        """
-        Return a Symbol that can be used to compute symbolic expressions
-        referring to this continuous field.
-        """
-        assert hasattr(self, '_symbol'), 'Symbol has not been defined.'
-        return self._symbol
-    
-    symbol = property(_get_symbol)
-    s = property(_get_symbol)
-
-
-class NamedObjectI(object):
-    __metaclass__ = ABCMeta
-    
-    def __new__(cls, name, pretty_name=None, **kwds):
-        """
-        Create an abstract named object that contains a symbolic value.
-        name : string
-            A name for the field.
-        pretty_name: string or unicode, optional.
-            A pretty name used for display whenever possible (unicode supported).
-            Defaults to name.
-        kwds: dict
-            Keywords arguments for base class.
-        """
-        check_instance(name, str)
-        check_instance(pretty_name, (str,unicode), allow_none=True)
-        
-        pretty_name = first_not_None(pretty_name, name)
-        if isinstance(pretty_name, unicode):
-            pretty_name = pretty_name.encode('utf-8')
-        check_instance(pretty_name, str)
-        
-        obj = super(NamedObjectI, cls).__new__(cls, **kwds)
-        obj._name   = name
-        obj._pretty_name = pretty_name
-        return obj
-        
-    @abstractmethod
-    def short_description(self):
-        """Short description of this field as a string."""
-        pass
-
-    @abstractmethod
-    def long_description(self):
-        """Long description of this field as a string."""
-        pass
-
-    def _get_name(self):
-        """Return the name of this field."""
-        return self._name
-    def _get_pretty_name(self):
-        """Return the pretty name of this field."""
-        return self._pretty_name
-    
-    def __str__(self):
-        return self.long_description()
-    
-    def rename(self, name, pretty_name=None):
-        """Change the name or pretty name of this object."""
-        pretty_name = first_not_None(pretty_name, name)
-        check_instance(name, str)
-        self._name = name
-        if isinstance(pretty_name, unicode):
-            pretty_name = pretty_name.encode('utf-8')
-        check_instance(pretty_name, str)
-        self._pretty_name = pretty_name
-        return self
-   
-    name = property(_get_name)
-    pretty_name = property(_get_pretty_name)
-
-
-class NamedScalarContainerI(NamedObjectI, SymbolContainerI):
-    @property
-    def ndim(self):
-        """Number of dimensions of this this tensor."""
-        return 0
-
-
-class NamedTensorContainerI(NamedObjectI, SymbolContainerI):
-    @debug
-    def __new__(cls, contained_objects, **kwds):
-        check_instance(contained_objects, npw.ndarray)
-        obj = super(NamedTensorContainerI, cls).__new__(cls, **kwds)
-        obj._contained_objects = contained_objects
-        return obj
-    
-    @property
-    def size(self):
-        """Full size of this container as if it was a 1D tensor."""
-        return self._contained_objects.size
-
-    @property
-    def shape(self):
-        """Shape of this tensor."""
-        return self._contained_objects.shape
-    
-    @property
-    def ndim(self):
-        """Number of dimensions of this this tensor."""
-        return self._contained_objects.ndim
-
-    def new_empty_array(self, dtype=object):
-        """Return a new empty array of the same shape as self."""
-        if (dtype is object):
-            array = npw.empty(shape=self.shape, dtype=dtype)
-            array[...] = None
-        else:
-            array = npw.zeros(shape=self.shape, dtype=dtype)
-        return array
-
-    def iter_fields(self):
-        """Return an iterator on unique scalar object along with 1d index."""
-        for (i,obj) in enumerate(self._contained_objects.ravel()):
-            yield (i,obj)
-
-    def nd_iter(self):
-        """Return an nd-indexed iterator of contained objects."""
-        for idx in npw.ndindex(*self._contained_objects.shape):
-            yield (idx, self._contained_objects[idx])
-    
-    def __iter__(self):
-        """Return an iterator on unique scalar objects."""
-        return self._contained_objects.ravel().__iter__()
-
-    def __contains__(self, obj):
-        """Check if a scalar object is contained in self."""
-        return obj in self._contained_objects
-    
-    @abstractmethod
-    def __getitem__(self, slc):
-        pass
+from hysop.tools.sympy_utils import nabla, partial, subscript, subscripts, \
+                                    exponent, exponents, xsymbol
+from hysop.symbolic import SpaceSymbol
+from hysop.tools.interface import NamedObjectI, SymbolContainerI, \
+        NamedScalarContainerI, NamedTensorContainerI
 
 
 class FieldContainerI(TaggedObject):
@@ -237,58 +106,244 @@ class FieldContainerI(TaggedObject):
             The topology state on which to discretize this ScalarField.
         """
         pass
+    
+    @classmethod
+    def from_sympy_expressions(cls, name, exprs, space_symbols, 
+                                    scalar_name_prefix=None, scalar_pretty_name_prefix=None,
+                                    pretty_name=None,  **kwds):
+        """
+        Create a field wich has the same shape as exprs, with optional names.
+        Expressions should be of kind sympy.Expr and are converted to FieldExpression: this
+        means they all have to contain at least one FieldExpression.
+        Note that field.symbol is always a SymbolicField which is a FieldExpression. 
+        FieldExpression make sure boundary conditions match between fields for derivatives 
+        and integrations.
+        """
+        if isinstance(exprs, sm.Expr):
+            raise NotImplementedError('Call self.from_sympy_expression instead.')
+        check_instance(exprs, npw.ndarray, values=sm.Expr)
+        check_instance(name, str)
+        check_instance(pretty_name, (str, unicode), allow_none=True)
+        check_instance(scalar_name_prefix, str, allow_none=True)
+        check_instance(scalar_pretty_name_prefix, (str, unicode), allow_none=True)
+        if isinstance(pretty_name, unicode):
+            pretty_name = pretty_name.encode('utf-8')
+        if isinstance(scalar_pretty_name_prefix, unicode):
+            scalar_pretty_name_prefix = scalar_pretty_name_prefix.encode('utf-8')
+
+        fields = npw.empty(shape=exprs.shape, dtype=object)
+        fields[...] = None
+        for idx in npw.ndindex(*exprs.shape):
+            if (scalar_name_prefix is not None):
+                sname = TensorField.default_name_formatter(scalar_name_prefix, idx)
+                if (scalar_pretty_name_prefix is not None):
+                    spname = TensorField.default_pretty_name_formatter(
+                            scalar_pretty_name_prefix, idx)
+                else:
+                    spname = TensorField.default_pretty_name_formatter(
+                            scalar_name_prefix, idx)
+            else:
+                # names will be autogenerated from sympy expression
+                sname  = None
+                spname = None
+
+            fields[idx] = cls.from_sympy_expression(expr=exprs[idx], 
+                            space_symbols=space_symbols,
+                            name=sname, pretty_name=spname, **kwds)
+        return TensorField.from_field_array(name=name, pretty_name=pretty_name,
+                                            fields=fields)
+
+    @classmethod
+    def from_sympy_expression(cls, expr, space_symbols, **kwds):
+        from hysop.symbolic.field import FieldExpressionBuilder
+        from hysop.tools.field_utils import print_all_names
+        assert 'lboundaries' not in kwds
+        assert 'rboundaries' not in kwds
+        assert 'domain' not in kwds
+
+        # determine names if not given
+        if ('name' not in kwds) or (kwds['name'] is None):
+            (name, pretty_name, var_name, latex_name) = print_all_names(expr)
+            kwds['name'] = name
+            kwds['pretty_name'] = pretty_name
+            kwds['var_name'] = var_name
+            kwds['latex_name'] = latex_name
+
+        # determine domain and boundary conditions
+        fe = FieldExpressionBuilder.to_field_expression(
+                expr=expr, space_symbols=space_symbols, strict=True)
+        kwds['domain']      = fe.domain
+        kwds['lboundaries'] = fe.lboundaries
+        kwds['rboundaries'] = fe.rboundaries
+
+        # deduce data type from field expression if not specified
+        kwds['dtype'] = first_not_None(kwds.get('dtype', None), fe.dtype)
+
+        # finally return create and return the ScalarField
+        return ScalarField(**kwds)
+        
 
     def gradient(self, name=None, pretty_name=None, 
+                       scalar_name_prefix=None, scalar_pretty_name_prefix=None,
                        directions=None, axis=-1, 
+                       space_symbols=None,
                        dtype=None, **kwds):
         """
         Create a field capable of storing the gradient of self, 
         possibly altered.
         """
-        from hysop.tools.sympy_utils import nabla, partial, subscript, xsymbol
-        domain         = self.domain
-        ndim           = self.ndim
+        dim    = self.dim  # dimension of the domain
+        ndim   = self.ndim # number of dimension of the np.ndarray
+        frame = self.domain.frame
 
-        directions = to_tuple(first_not_None(directions, range(self.dim)))
+        directions = to_tuple(first_not_None(directions, range(dim)))
+        space_symbols = to_tuple(first_not_None(space_symbols, frame.coords))
         check_instance(directions, tuple, minval=0, maxval=self.dim-1, minsize=1, unique=True) 
         check_instance(axis, int, minval=-ndim, maxval=ndim-1)
+        check_instance(space_symbols, tuple, values=SpaceSymbol, size=dim, unique=True)
 
         ndirs = len(directions)
         if ndim>0:
-            axis   = (axis+ndim)%ndim
+            axis  = (axis+ndim)%ndim
             shape = self.shape[:axis+1] + (ndirs,) + self.shape[axis+1:]
         else:
             shape = (ndirs,)
-    
+
+        name = first_not_None(name, 'grad_{}'.format(self.name))
+        pretty_name = first_not_None(pretty_name, '{}{}'.format(nabla.encode('utf8'),
+                                     self.pretty_name))
+
         if shape==(1,):
-            name  = first_not_None(name, '{d}{}_dx{}'.format(
-                                        F.name, directions[0], d='d'))
-            pname = first_not_None(pretty_name, u'{d}{}/{d}{x}{}'.format(
-                        F.pretty_name.decode('utf-8'), 
-                        subscript(directions[0]), d=partial, x=xsymbol))
-            return self.field_like(name=name, pretty_name=pretty_name)
+            expr = self.symbol(frame.time, *space_symbols).diff(space_symbols[directions[0]])
+            return self.from_sympy_expression(expr=expr, space_symbols=space_symbols,
+                                              name=name, pretty_name=pretty_name,
+                                              dtype=dtype, **kwds)
         else:
-            name           = first_not_None(name, 'grad_{}'.format(self.name))
-            pretty_name    = first_not_None(pretty_name, '{}{}'.format(nabla.encode('utf8'), 
-                                            self.pretty_name))
-            def make_field(idx, **fkwds): 
+            exprs = npw.empty(shape=shape, dtype=object)
+            for idx in npw.ndindex(*shape):
                 i = idx[:axis+1] + idx[axis+2:]
                 d = directions[idx[axis+1]]
-                
-                if (ndim==0):
-                    Fi = self
+                if self.is_tensor:
+                    exprs[idx] = self[i].symbol(frame.time, 
+                            *space_symbols).diff(space_symbols[d])
                 else:
-                    Fi = self[i]
+                    assert i==(), i
+                    exprs[idx] = self.symbol(frame.time, *space_symbols).diff(space_symbols[d])
+            return self.from_sympy_expressions(
+                    exprs=exprs, space_symbols=space_symbols,
+                    name=name, pretty_name=pretty_name,
+                    scalar_name_prefix=scalar_name_prefix,
+                    scalar_pretty_name_prefix=scalar_pretty_name_prefix,
+                    dtype=dtype, **kwds)
+
+    def laplacian(self, name=None, pretty_name=None, 
+                  scalar_name_prefix=None, scalar_pretty_name_prefix=None,
+                  dtype=None, **kwds):
+        from hysop.symbolic.field import laplacian
+        frame = self.domain.frame
+        exprs = laplacian(self.symbol(*frame.vars), frame)
+
+        name = first_not_None(name, 'laplacian_{}'.format(self.name))
+        pretty_name = first_not_None(pretty_name, u'\u0394{}'.format(
+                                     self.pretty_name.decode('utf-8')))
+        
+        if isinstance(exprs, npw.ndarray):
+            if (exprs.size == 1):
+                expr = exprs.item()
+                return self.from_sympy_expression(expr=expr, space_symbols=frame.coords,
+                                                  name=name, pretty_name=pretty_name,
+                                                  dtype=dtype, **kwds)
+            else:
+                return self.from_sympy_expressions(
+                        exprs=exprs, space_symbols=frame.coords,
+                        name=name, pretty_name=pretty_name,
+                        scalar_name_prefix=scalar_name_prefix,
+                        scalar_pretty_name_prefix=scalar_pretty_name_prefix,
+                        dtype=dtype, **kwds)
+        else:
+            expr = exprs
+            return self.from_sympy_expression(expr=expr, space_symbols=frame.coords,
+                                              name=name, pretty_name=pretty_name,
+                                              dtype=dtype, **kwds)
 
-                fkwds['dtype']       = first_not_None(dtype, Fi.dtype)
-                fkwds['name']        = '{d}{}_{d}x{}'.format(Fi.name, d, d='d')
-                fkwds['pretty_name'] = u'{d}{}/{d}{x}{}'.format(Fi.pretty_name.decode('utf-8'), 
-                                        subscript(d), d=partial, x=xsymbol)
 
-                return Fi.field_like(**fkwds)
+    def div(self, name=None, pretty_name=None, 
+                  scalar_name_prefix=None, scalar_pretty_name_prefix=None,
+                  axis=-1, dtype=None, **kwds):
+        """
+        Create a field capable of storing the divergence of self, 
+        on chosen axis.
+        """
+        from hysop.symbolic.field import div
+        frame = self.domain.frame
+        exprs = npw.asarray(div(self.symbol(*frame.vars), frame))
+
+        name = first_not_None(name, 'div_{}'.format(self.name))
+        pretty_name = first_not_None(pretty_name, u'{}\u22c5{}'.format(nabla,
+                                     self.pretty_name.decode('utf-8')))
+        
+        if exprs.size in (0,1):
+            expr = npw.asscalar(exprs)
+            return self.from_sympy_expression(expr=expr, space_symbols=frame.coords,
+                                              name=name, pretty_name=pretty_name,
+                                              dtype=dtype, **kwds)
+        else:
+            return self.from_sympy_expressions(exprs=exprs, space_symbols=frame.coords,
+                                               name=name, pretty_name=pretty_name,
+                                               scalar_name_prefix=scalar_name_prefix,
+                                               scalar_pretty_name_prefix=scalar_pretty_name_prefix,
+                                               dtype=dtype, **kwds)
+
+    def curl(self, name=None, pretty_name=None, 
+                   scalar_name_prefix=None, scalar_pretty_name_prefix=None,
+                   dtype=None, **kwds):
+        """
+        Create a field capable of storing the curl of self, 
+
+        Only 2D and 3D fields are supported as the curl brings 
+        a 1-vector to a 2-vector:
+        
+        - A vector to a pseudoscalar or a pseudoscalar to a vector in 2D 
+        - A vector to a pseudovector or a pseudovector to a vector in 3D
+
+        In 1D the curl is 0, and in 4D the curl would be a 6D 'field'.
+        """
+        from hysop.symbolic.field import curl
+        
+        
+        if (self.dim==2):
+            msg='Can only take curl for a 2D field with one or two components.'
+            assert self.nb_components in (1,2), msg
+        elif (self.dim==3):
+            msg='Can only take curl for a 3D field with three components.'
+            assert self.nb_components in (3,), msg
+        else:
+            msg='Can only take curl for a 2D or 3D vector field.'
+            assert (self.dim in (2,3)), msg
+
+        frame = self.domain.frame
+        exprs = curl(self.symbol(*frame.vars), frame)
+
+        name = first_not_None(name, 'curl_{}'.format(self.name))
+        pretty_name = first_not_None(pretty_name, u'{}\u2227{}'.format(nabla,
+                                     self.pretty_name.decode('utf-8')))
+
+        if isinstance(exprs, npw.ndarray):
+            return self.from_sympy_expressions(
+                    exprs=exprs, space_symbols=frame.coords,
+                    name=name, pretty_name=pretty_name,
+                    scalar_name_prefix=scalar_name_prefix,            
+                    scalar_pretty_name_prefix=scalar_pretty_name_prefix,
+                    dtype=dtype, **kwds)
+        else:
+            return self.from_sympy_expression(expr=exprs, space_symbols=frame.coords,
+                                              name=name, pretty_name=pretty_name,
+                                              dtype=dtype, **kwds)
+
+    def rot(self, *args, **kwds):
+        """See curl."""
+        return self.curl(*args, **kwds)
 
-            return TensorField(name=name, pretty_name=pretty_name, domain=domain, shape=shape,
-                                make_field=make_field, **kwds)
     
     def get_attributes(self, *attrs):
         """
@@ -318,10 +373,17 @@ class FieldContainerI(TaggedObject):
                     if not are_equal(ai, bi):
                         return False
                 return True
-            elif isinstance(a, npw.ndarray):
+            if isinstance(a, dict):
+                for k in set(a.keys()+b.keys()):
+                    if (k not in a) or (k not in b):
+                        return False
+                    ak, bk = a[k], b[k]
+                    if not are_equal(ak, bk):
+                        return False
+                    return True
+            if isinstance(a, npw.ndarray):
                 return npw.array_equal(a,b)
-            else:
-                return (a==b)
+            return (a==b)
         objects = self.get_attributes(*attr)
         obj0 = objects[0]
         for obj in objects[1:]:
@@ -338,12 +400,24 @@ class FieldContainerI(TaggedObject):
         if self.has_unique_attribute(*attr):
            return self.fields[0].get_attributes(*attr)[0]
         msg='{} is not unique accross contained fields.'
-        msg=msg.format(attr.title())
+        msg=msg.format('.'.join(str(x) for x in attr))
         raise AttributeError(msg)
 
     def has_unique_dtype(self):
         """Return true if all contained discrete fields share the same dtype."""
         return self.has_unique_attribute('dtype')
+    def has_unique_lboundaries(self):
+        """Return true if all contained continuous fields share the same lboundaries."""
+        return self.has_unique_attribute("lboundaries")
+    def has_unique_rboundaries(self):
+        """Return true if all contained continuous fields share the same rboundaries."""
+        return self.has_unique_attribute("rboundaries")
+    def has_unique_boundaries(self):
+        """Return true if all contained continuous fields share the same boundaries."""
+        return self.has_unique_attribute("boundaries")
+    def has_unique_periodicity(self):
+        """Return true if all contained continuous fields share the same periodicity."""
+        return self.has_unique_attribute("periodicity")
 
     @property
     def dtype(self):
@@ -352,13 +426,34 @@ class FieldContainerI(TaggedObject):
         else raise an AttributeError.
         """
         return self.get_unique_attribute('dtype')
-
-    def _get_domain(self):
-        """Return the physical domain where this field is defined."""
-        return self._domain
-    def _get_dim(self):
-        """Return the dimension of the physical domain."""
-        return self._dim
+    @property
+    def lboundaries(self):
+        """
+        Try to return the unique lboundaries common to all contained fields,
+        else raise an AttributeError.
+        """
+        return self.get_unique_attribute("lboundaries")
+    @property
+    def rboundaries(self):
+        """
+        Try to return the unique rboundaries common to all contained fields,
+        else raise an AttributeError.
+        """
+        return self.get_unique_attribute("rboundaries")
+    @property
+    def boundaries(self):
+        """
+        Try to return the unique boundaries common to all contained fields,
+        else raise an AttributeError.
+        """
+        return self.get_unique_attribute("boundaries")
+    @property
+    def periodicity(self):
+        """
+        Try to return the unique periodicity common to all contained fields,
+        else raise an AttributeError.
+        """
+        return self.get_unique_attribute("periodicity")
 
     def __eq__(self, other):
         return (self is other)
@@ -367,6 +462,13 @@ class FieldContainerI(TaggedObject):
     def __hash__(self):
         return id(self)
     
+
+    def _get_domain(self):
+        """Return the physical domain where this field is defined."""
+        return self._domain
+    def _get_dim(self):
+        """Return the dimension of the physical domain."""
+        return self._dim
     domain = property(_get_domain)
     dim = property(_get_dim)
 
@@ -396,14 +498,12 @@ class ScalarField(NamedScalarContainerI, FieldContainerI):
     scal_discr2 = scal.discrete_fields[topo2]
     """
 
-    @property
-    def is_tensor(self):
-        return False
-
     @debug
     def __new__(cls, domain, name, pretty_name=None,
-                initial_values=0, dtype=HYSOP_REAL,
-                is_tmp=False, **kwds):
+                var_name=None, latex_name=None,
+                initial_values=None, dtype=HYSOP_REAL,
+                lboundaries=None, rboundaries=None,
+                is_tmp=False, mem_tag=None, **kwds):
         """
         Create or get an existing continuous ScalarField (scalar or vector) on a specific domain.
 
@@ -416,6 +516,12 @@ class ScalarField(NamedScalarContainerI, FieldContainerI):
         pretty_name: string or unicode, optional.
             A pretty name used for display whenever possible.
             Defaults to name.
+        var_name: string, optional.
+            A variable name used for code generation.
+            This will be passed to the symbolic representation of this field.
+        latex_name: string, optional.
+            A variable name used for latex generation.
+            This will be passed to the symbolic representation of this field.
         dtype: npw.dtype, optional, defaults to HYSOP_REAL
             Underlying data type of this field
         initial_values: numeric value, or tuple of numeric values, optional
@@ -425,9 +531,13 @@ class ScalarField(NamedScalarContainerI, FieldContainerI):
             
             If None, leaves the memory uninitialized.
             If a single value is given, the whole field is initialized to this value,
-             the default being 0.
+             the default being None (ie. no initialization at all).
             If tuple, computational mesh will be initialized with the first value,
-                      and ghosts will be initialized with the second value.
+                    and ghosts will be initialized with the second value.
+        lboundaries: array_like of BoundaryCondition, optional
+            Left boundary conditions, defaults to PERIODIC on each axis.
+        rboundaries: array_like of BoundaryCondition, optional
+            Right boundary conditions, defaults to PERIODIC on each axis.
         is_tmp: bool
             Specify that this field is a temporary continuous field.
             Basically a ScalarField that yields a temporary discrete field upon discretization.
@@ -440,7 +550,22 @@ class ScalarField(NamedScalarContainerI, FieldContainerI):
             /!\ ***************************************************** /!\
         kwds: dict
             Base class keyword arguments.
+        
+        Attributes
+        ----------
+        boundaries: tuple of numpy.ndarray of BoundaryCondition
+            Left and right boundary conditions as a tuple.
+        periodicity: numpy.ndarray of bool
+            Numpy array mask, True is axis is periodic, else False.
         """
+        check_instance(name, str)
+        check_instance(pretty_name, (str, unicode), allow_none=True)
+        check_instance(latex_name, str, allow_none=True)
+        check_instance(var_name, str, allow_none=True)
+        check_instance(is_tmp, bool)
+
+        if (mem_tag is not None):
+            assert is_tmp, 'Can only specify mem_tag for temporary fields.'
 
         # Data type of the field
         if (dtype==npw.bool) or (dtype==bool):
@@ -451,21 +576,54 @@ class ScalarField(NamedScalarContainerI, FieldContainerI):
             dtype = HYSOP_BOOL
         dtype = npw.dtype(dtype)
         
+        # Name and pretty name
+        pretty_name = first_not_None(pretty_name, name)
+        if isinstance(pretty_name, unicode):
+            pretty_name = pretty_name.encode('utf-8')
+        check_instance(pretty_name, str)
+        
         # Initial values
         if not isinstance(initial_values,(list,tuple)):
             initial_values = (initial_values, initial_values)
         assert len(initial_values)==2
         initial_values = tuple(initial_values)
         check_instance(initial_values, tuple, size=2)
-        check_instance(is_tmp, bool)
-    
+        
+        # Field boundary conditions
+        lboundaries = npw.asarray(first_not_None(lboundaries, 
+            cls.default_boundaries_from_domain(domain.lboundaries)))
+        rboundaries = npw.asarray(first_not_None(rboundaries, 
+            cls.default_boundaries_from_domain(domain.rboundaries)))
+        check_instance(lboundaries, npw.ndarray, values=BoundaryCondition, 
+                ndim=1, size=domain.dim, dtype=object, allow_none=True)
+        check_instance(rboundaries, npw.ndarray, values=BoundaryCondition, 
+                ndim=1, size=domain.dim, dtype=object, allow_none=True)
+        assert lboundaries.size == rboundaries.size == domain.dim
+        for i,(lb,rb) in enumerate(zip(lboundaries,rboundaries)):
+            if (lb==BoundaryCondition.PERIODIC) ^ (rb==BoundaryCondition.PERIODIC):
+                msg='Periodic BoundaryCondition mismatch on axis {}.'.format(i)
+                raise ValueError(msg)
+        check_instance(lboundaries, npw.ndarray, values=BoundaryCondition, 
+                ndim=1, size=domain.dim, dtype=object)
+        check_instance(rboundaries, npw.ndarray, values=BoundaryCondition, 
+                ndim=1, size=domain.dim, dtype=object)
+
+        periodic    = BoundaryCondition.PERIODIC
+        periodicity = (lboundaries==periodic)
+        
         obj = super(ScalarField, cls).__new__(cls, domain=domain,
                 name=name, pretty_name=pretty_name, 
+                var_name=var_name, latex_name=latex_name,
                 tag_prefix='f', tagged_cls=ScalarField, **kwds)
         obj._dtype  = dtype
         obj._initial_values = initial_values
         obj._is_tmp = is_tmp
-        
+        obj._mem_tag = mem_tag
+        obj._lboundaries = lboundaries
+        obj._rboundaries = rboundaries
+        obj._periodicity = periodicity
+    
+        # Symbolic representation of this field
         from hysop.symbolic.field import SymbolicField
         obj._symbol = SymbolicField(field=obj)
 
@@ -473,91 +631,110 @@ class ScalarField(NamedScalarContainerI, FieldContainerI):
         # keys are hysop.topology.topology.Topology,
         # values are hysop.fields.discrete_field.DiscreteField.
         obj._discrete_fields = {}
+        cls.__check_vars(obj)
         return obj
 
-    def __eq__(self, other):
-        return (self is other)
-    def __ne__(self, other):
-        return (self is not other)
-    def __hash__(self):
-        return id(self)
-
-    def __check_vars(self):
-        """Check properties and types."""
-        check_instance(self.dtype, np.dtype)
-        check_instance(self.domain, Domain)
-        check_instance(self.name, str)
-        check_instance(self.pretty_name, str)
-        check_instance(self.dim, int, minval=1)
-        check_instance(self.nb_components, int, minval=1)
-        check_instance(self.discrete_fields, dict)
-        check_instance(self.initial_values, tuple, size=2)
+    @classmethod
+    def default_boundaries_from_domain(cls, boundaries):
+        check_instance(boundaries, npw.ndarray, values=BoxBoundaryCondition)
+        field_boundaries = npw.empty_like(boundaries)
+        field_boundaries[...] = None
+        for (i,bd) in enumerate(boundaries):
+            if (bd is BoxBoundaryCondition.PERIODIC):
+                fbd = BoundaryCondition.PERIODIC
+            elif (bd is BoxBoundaryCondition.SYMMETRIC): # (normal to boundary velocity = 0)
+                # let any advected scalar to be 0 in boundaries
+                fbd = BoundaryCondition.HOMOGENEOUS_DIRICHLET
+            elif (bd is BoxBoundaryCondition.OUTFLOW): # (velocity normal to boundary)
+                # let any advected scalar to go trough the boundary
+                fbd = BoundaryCondition.HOMOGENEOUS_NEUMANN
+            else:
+                msg='FATAL ERROR: Unknown domain boundary condition {}.'
+                msg=msg.format(bd)
+                raise NotImplementedError(msg)
+            field_boundaries[i] = fbd
+        return field_boundaries
     
+    @classmethod
+    def __check_vars(cls, obj):
+        """Check properties and types."""
+        check_instance(obj.dtype, npw.dtype)
+        check_instance(obj.domain, Domain)
+        check_instance(obj.name, str)
+        check_instance(obj.pretty_name, str)
+        check_instance(obj.dim, int, minval=1)
+        check_instance(obj.nb_components, int, minval=1)
+        check_instance(obj.discrete_fields, dict)
+        check_instance(obj.initial_values, tuple, size=2)
+        check_instance(obj.lboundaries, npw.ndarray, values=BoundaryCondition, 
+                ndim=1, size=obj.domain.dim, dtype=object)
+        check_instance(obj.rboundaries, npw.ndarray, values=BoundaryCondition, 
+                ndim=1, size=obj.domain.dim, dtype=object)
+        check_instance(obj.periodicity, npw.ndarray, dtype=bool, 
+                ndim=1, size=obj.domain.dim)
+        check_instance(obj.is_tmp, bool)
+
     def field_like(self, name, pretty_name=None,
+            latex_name=None, var_name=None,
             domain=None, dtype=None, is_tmp=None, 
+            lboundaries=None, rboundaries=None,
             initial_values=None, **kwds):
         """Create a ScalarField like this object, possibly altered."""
         check_instance(name, str)
-        domain = first_not_None(domain, self.domain)
-        dtype  = first_not_None(dtype, self.dtype)
-        is_tmp = first_not_None(is_tmp, self.is_tmp)
+        domain         = first_not_None(domain, self.domain)
+        dtype          = first_not_None(dtype, self.dtype)
+        is_tmp         = first_not_None(is_tmp, self.is_tmp)
+        lboundaries    = first_not_None(lboundaries, self.lboundaries)
+        rboundaries    = first_not_None(rboundaries, self.rboundaries)
         initial_values = first_not_None(initial_values, self.initial_values)
         return ScalarField(name=name, pretty_name=pretty_name,
+                var_name=var_name, latex_name=latex_name,
                 domain=domain, dtype=dtype, is_tmp=is_tmp, 
+                lboundaries=lboundaries, rboundaries=rboundaries, 
                 initial_values=initial_values, **kwds)
 
-    def tmp_like(self, name, pretty_name=None,
-            domain=None, initial_values=None, dtype=None, **kwds):
+    def tmp_like(self, name, **kwds):
         """Create a TemporaryField like self, possibly altered."""
-        assert ('shape' not in kwds)
-        assert ('nb_components' not in kwds) or kwds['nb_components']==1
-        check_instance(name, str)
-        domain = first_not_None(domain, self.domain)
-        dtype  = first_not_None(dtype,  self.dtype)
-        initial_values = first_not_None(initial_values, self.initial_values)
-        return ScalarField(name=name, pretty_name=pretty_name,
-                domain=domain, initial_values=initial_values, dtype=dtype, 
-                is_tmp=True, register_object=False,
-                **kwds)
+        return self.field_like(name=name, is_tmp=True, **kwds)
     
     def short_description(self):
         """Short description of this field."""
-        s = '{}[name={}, pname={}, dtype={}, initial_values={}]'
-        s = s.format(self.full_tag, self.name, self.pretty_name, 
-                     self.dtype, self.initial_values)
+        s = '{}[pname={}, dim={}, dtype={}, bc=[{}], iv={}]'
+        s = s.format(self.full_tag, self.name, self.dim, 
+                     self.dtype, 
+                     self.format_boundaries(),
+                     self.initial_values)
         return s
+    
+    def format_boundaries(self):
+        from hysop.constants import format_boundaries as fb
+        return fb(*self.boundaries)
 
     def long_description(self):
         """Long description of this field."""
-        s=textwrap.dedent(
+        s = textwrap.dedent(
         '''
-            {}
-              *name:           {}
-              *pname:          {}
-              *dim:            {}
-              *dtype:          {}
-              *symbolic repr.: {}
-              *initial values: {}
-              *topology tags:  [{}]
-        '''.format(self.full_tag,
-                self.name, self.pretty_name, self.dim, 
-                self.dtype, self.symbol, self.initial_values,
-                ','.join([k.full_tag for k in self.discrete_fields.keys()])))[1:]
-        return s
-    
-    @property
-    def is_tmp(self):
-        """Is this ScalarField a temporary field ?"""
-        return self._is_tmp
-    
-    @property
-    def fields(self):
-        return (self,)
-    
-    @property
-    def nb_components(self):
-        return 1
+        {}
+          *name:           {}
+          *pretty_name:    {}
+          *var_name:       {}
+          *latex_name:     {}
+          *dim:            {}
+          *dtype:          {}
+          *left  boundary: {}
+          *right boundary: {}
+          *initial values: {}
+          *topology tags:  [{}]
+        ''').format(self.full_tag,
+                self.name, self.pretty_name, 
+                self.var_name, self.latex_name,
+                self.dim, self.dtype,
+                self.lboundaries.tolist(), self.rboundaries.tolist(), 
+                self.initial_values,
+                ','.join([k.full_tag for k in self.discrete_fields.keys()]))
+        return s[1:]
                 
+    
     @debug
     def discretize(self, topology, topology_state=None):
         """
@@ -581,7 +758,7 @@ class ScalarField(NamedScalarContainerI, FieldContainerI):
         """
         from hysop.fields.discrete_field import DiscreteField
         topology_state = first_not_None(topology_state, topology.default_state())
-        check_instance(topology,  Topology)
+        check_instance(topology,       Topology)
         check_instance(topology_state, TopologyState)
 
         if (topology not in self.discrete_fields):
@@ -606,11 +783,55 @@ class ScalarField(NamedScalarContainerI, FieldContainerI):
         of this field.
         """
         return self._discrete_fields
-
+    def _get_lboundaries(self):
+        """Left boundary conditions."""
+        return self._lboundaries
+    def _get_rboundaries(self):
+        """Right boundary conditions."""
+        return self._rboundaries
+    def _get_boundaries(self):
+        """Left and right boundary conditions as a tuple."""
+        return (self._lboundaries, self._rboundaries)
+    def _get_periodicity(self):
+        """Numpy array mask, True is axis is periodic, else False."""
+        return self._periodicity
+    def _get_is_tmp(self):
+        """Is this ScalarField a temporary field ?"""
+        return self._is_tmp
+    def _get_mem_tag(self):
+        return self._mem_tag
+    
     dtype = property(_get_dtype)
     initial_values = property(_get_initial_values)
     discrete_fields = property(_get_discrete_fields)
+    lboundaries = property(_get_lboundaries)
+    rboundaries = property(_get_rboundaries)
+    boundaries = property(_get_boundaries)
+    periodicity = property(_get_periodicity)
+    is_tmp = property(_get_is_tmp)
+    mem_tag = property(_get_mem_tag)
     
+    @property
+    def is_tensor(self):
+        return False
+    
+    @property
+    def fields(self):
+        return (self,)
+    
+    @property
+    def nb_components(self):
+        return 1
+
+    def __str__(self):
+        return self.long_description()
+    def __eq__(self, other):
+        return (self is other)
+    def __ne__(self, other):
+        return (self is not other)
+    def __hash__(self):
+        return id(self)
+
 
 class TensorField(NamedTensorContainerI, FieldContainerI):
     """
@@ -662,7 +883,7 @@ class TensorField(NamedTensorContainerI, FieldContainerI):
         pretty_name_formatter = first_not_None(pretty_name_formatter, 
                                                cls.default_pretty_name_formatter)
         skip_field = first_not_None(skip_field, cls.default_skip_field)
-        make_field = first_not_None(make_field, lambda idx,**kwds: ScalarField(**kwds))
+        make_field = first_not_None(make_field, cls.default_make_field)
         base_kwds = first_not_None(base_kwds, {})
         
         check_instance(domain, Domain)
@@ -711,7 +932,8 @@ class TensorField(NamedTensorContainerI, FieldContainerI):
         from hysop.fields.discrete_field import DiscreteTensorField
         dfields = npw.empty(shape=self.shape, dtype=object)
         for (idx, field) in self.nd_iter():
-            dfields[idx] = field.discretize(topology=topology, topology_state=topology_state)
+            dfields[idx] = field.discretize(topology=topology, 
+                                            topology_state=topology_state)
         return DiscreteTensorField(field=self, dfields=dfields)
 
     @classmethod
@@ -737,7 +959,7 @@ class TensorField(NamedTensorContainerI, FieldContainerI):
     
     @classmethod
     def from_field_array(cls, name, fields, pretty_name=None, **kwds):
-        """Create a TensorField from np.ndarray of fields."""
+        """Create a TensorField from numpy.ndarray of fields."""
         assert (fields.size > 1)
         check_instance(name, str)
         check_instance(pretty_name, (str, unicode), allow_none=True)
@@ -775,11 +997,14 @@ class TensorField(NamedTensorContainerI, FieldContainerI):
     @classmethod
     def default_pretty_name_formatter(cls, basename, idx):
         check_instance(basename, str)
-        from hysop.tools.sympy_utils import subscripts
         assert len(basename)>0
         pname = basename + subscripts(ids=idx, sep='').encode('utf-8')
         return pname
 
+    @classmethod
+    def default_make_field(cls, idx, **kwds):
+        return ScalarField(**kwds)
+    
     @classmethod
     def default_skip_field(cls, idx):
         return False
@@ -841,7 +1066,7 @@ class TensorField(NamedTensorContainerI, FieldContainerI):
         '''
             {}
               *name:           {}
-              *pname:          {}
+              *pretty_name:    {}
               *dim:            {}
               *shape:          {}
               *nb_components:  {}
@@ -851,21 +1076,10 @@ class TensorField(NamedTensorContainerI, FieldContainerI):
         s+='      '+'\n      '.join(str(self.symbol).split('\n'))
         return s
 
-    def field_like(self, name, pretty_name=None, **kwds):
+    def field_like(self, name, pretty_name=None, 
+                        shape=None, nb_components=None, 
+                        fn='field_like', **kwds):
         """Create a TensorField like this object, possibly altered."""
-        pretty_name = first_not_None(pretty_name, name)
-        check_instance(name, str)
-        check_instance(pretty_name, str)
-        fields = npw.empty(shape=self.shape, dtype=object)
-        for (idx,field) in self.nd_iter():
-            fname  = self._name_formatter(basename=name, idx=idx)
-            pfname = self._pretty_name_formatter(basename=pretty_name, idx=idx)
-            fields[idx] = field.field_like(name=fname, pretty_name=pfname, **kwds)
-        return self.from_field_array(name=name, pretty_name=pretty_name, fields=fields)
-    
-    def tmp_like(self, name, pretty_name=None, 
-                       shape=None, nb_components=None, **kwds):
-        """Create a temporary field like self, possibly altered."""
         if (shape is None) and (nb_components is not None):
             shape = (nb_components,)
         del nb_components
@@ -874,24 +1088,30 @@ class TensorField(NamedTensorContainerI, FieldContainerI):
         
         pretty_name = first_not_None(pretty_name, name)
         check_instance(name, str)
-        check_instance(pretty_name, str)
+        check_instance(pretty_name, (str,unicode))
+        if not isinstance(pretty_name, str):
+            pretty_name = pretty_name.encode('utf-8')
         
         if (nb_components == 1):
-            return self.fields[0].tmp_like(name=name, pretty_name=pretty_name, **kwds)
+            return getattr(self.fields[0], fn)(name=name, pretty_name=pretty_name, **kwds)
         else:
             fields = npw.empty(shape=shape, dtype=object)
             if (self.shape == shape):
                 for (idx,field) in self.nd_iter():
                     fname  = self._name_formatter(basename=name, idx=idx)
                     pfname = self._pretty_name_formatter(basename=pretty_name, idx=idx)
-                    fields[idx] = field.tmp_like(name=fname, pretty_name=pfname, **kwds)
+                    fields[idx] = getattr(field, fn)(name=fname, pretty_name=pfname, **kwds)
             else:
                 field = self.fields[0]
                 for idx in npw.ndindex(*shape):
                     fname  = self._name_formatter(basename=name, idx=idx)
                     pfname = self._pretty_name_formatter(basename=pretty_name, idx=idx)
-                    fields[idx] = field.tmp_like(name=fname, pretty_name=pfname, **kwds)
+                    fields[idx] = getattr(field, fn)(name=fname, pretty_name=pfname, **kwds)
             return self.from_field_array(name=name, pretty_name=pretty_name, fields=fields)
+    
+    def tmp_like(self, name, **kwds):
+        """Create a temporary field like self, possibly altered."""
+        return self.field_like(name=name, fn='tmp_like', **kwds)
 
     def __getitem__(self, slc):
         fields = self._fields.__getitem__(slc)
diff --git a/hysop/fields/default_fields.py b/hysop/fields/default_fields.py
index a30c2a7ce4ea1e799af23a9be1ac48f97035d05e..39d9f2db563394553648a2fe66a1ba870211c02f 100644
--- a/hysop/fields/default_fields.py
+++ b/hysop/fields/default_fields.py
@@ -1,30 +1,61 @@
-from hysop.tools.types import first_not_None
+from hysop.tools.types import first_not_None, check_instance
 from hysop.tools.sympy_utils import greak, Greak, subscripts
-from hysop.fields.continuous_field import Field
+from hysop.fields.continuous_field import Field, TensorField 
+from hysop.tools.numpywrappers import npw
+from hysop.constants import BoxBoundaryCondition, BoundaryCondition
 
-def VelocityField(domain, name=None, pretty_name=None, 
-                    is_vector=True, **kwds):
+
+def VelocityField(domain, name=None, pretty_name=None, **kwds):
     name        = first_not_None(name, 'U')
     pretty_name = first_not_None(pretty_name, greak[20])
-    is_vector   = first_not_None(is_vector, True)
-    return Field(domain=domain, name=name, pretty_name=pretty_name, 
-            is_vector=is_vector, **kwds)
+    lboundaries, rboundaries = domain.lboundaries, domain.rboundaries
+    dim = domain.dim
+    def velocity_boundaries(boundaries, component):
+        check_instance(boundaries, npw.ndarray, values=BoxBoundaryCondition)
+        fboundaries = npw.empty_like(boundaries)
+        fboundaries[...] = None
+        for (i,bd) in enumerate(boundaries):
+            is_normal = (dim-i-1==component)
+            if (bd is BoxBoundaryCondition.PERIODIC):
+                fbd = BoundaryCondition.PERIODIC
+            elif (bd is BoxBoundaryCondition.SYMMETRIC): # (normal to boundary velocity = 0)
+                if is_normal:
+                    fbd = BoundaryCondition.HOMOGENEOUS_DIRICHLET
+                else:
+                    fbd = BoundaryCondition.HOMOGENEOUS_NEUMANN
+            elif (bd is BoxBoundaryCondition.OUTFLOW): # (velocity is normal to boundary)
+                if is_normal:
+                    fbd = BoundaryCondition.HOMOGENEOUS_NEUMANN
+                else:
+                    fbd = BoundaryCondition.HOMOGENEOUS_DIRICHLET
+            else:
+                msg='FATAL ERROR: Unknown domain boundary condition {}.'
+                msg=msg.format(bd)
+                raise NotImplementedError(msg)
+            fboundaries[i] = fbd
+        check_instance(fboundaries, npw.ndarray, values=BoundaryCondition)
+        return fboundaries
+    def _make_field(idx, **fkwds): 
+        # Adapt velocity boundaries to domain boundaries
+        component, = idx
+        fkwds['lboundaries'] = velocity_boundaries(lboundaries, component)
+        fkwds['rboundaries'] = velocity_boundaries(rboundaries, component)
+        return TensorField.default_make_field(idx=idx, **fkwds)
+    kwds.setdefault('make_field', _make_field)
+    kwds.setdefault('is_vector', True)
+    return Field(domain=domain, name=name, pretty_name=pretty_name, **kwds)
+
 
-def VorticityField(domain, name=None, pretty_name=None, 
-                    nb_components=None, **kwds):
+def VorticityField(velocity, name=None, pretty_name=None, **kwds):
+    # vorticity domain domain boundaries are deduced from velocity boundary conditions
+    check_instance(velocity, Field)
+    domain = velocity.domain
+    assert velocity.nb_components == domain.dim, 'Invalid velocity Field.'
     name        = first_not_None(name, 'W')
     pretty_name = first_not_None(pretty_name, greak[24])
-    if (nb_components is None):
-        if (domain.dim == 2):
-            nb_components = 1
-        elif (domain.dim == 3):
-            nb_components = 3
-        else:
-            msg='Cannot deduce the number of components of the vorticity '
-            msg+='for a {}d domain.'.format(domain.dim)
-            raise ValueError(msg)
-    return Field(domain=domain, name=name, pretty_name=pretty_name, 
-                    nb_components=nb_components, **kwds)
+    return velocity.curl(name=name, pretty_name=pretty_name, 
+            scalar_name_prefix=name, scalar_pretty_name_prefix=pretty_name,
+            **kwds)
 
 
 def DensityField(domain, name=None, pretty_name=None, **kwds):
@@ -42,12 +73,21 @@ def ViscosityField(domain, name=None, pretty_name=None, mu=False, **kwds):
         pretty_name = first_not_None(pretty_name, greak[12])
     return Field(domain=domain, name=name, pretty_name=pretty_name, **kwds)
 
+
 def LevelSetField(domain, name=None, pretty_name=None, **kwds):
     name = first_not_None(name, 'phi')
     pretty_name = first_not_None(pretty_name, Greak[21])
     return Field(domain=domain, name=name, pretty_name=pretty_name, **kwds)
 
+
 def PenalizationField(domain, name=None, pretty_name=None, **kwds):
     name = first_not_None(name, 'lambda')
     pretty_name = first_not_None(pretty_name, greak[10])
     return Field(domain=domain, name=name, pretty_name=pretty_name, **kwds)
+
+
+def CurvatureField(domain, name=None, pretty_name=None, **kwds):
+    name        = first_not_None(name, 'kappa')
+    pretty_name = first_not_None(pretty_name, greak[9])
+    return Field(domain=domain, name=name, pretty_name=pretty_name, **kwds)
+
diff --git a/hysop/fields/discrete_field.py b/hysop/fields/discrete_field.py
index 3205e2b35e6a4326cfc87a7e7e778e22522f62d1..e909ebbc733ff4ba59be47b8452ebf5651a55275 100644
--- a/hysop/fields/discrete_field.py
+++ b/hysop/fields/discrete_field.py
@@ -259,10 +259,17 @@ class DiscreteScalarFieldViewContainerI(object):
                     if not are_equal(ai, bi):
                         return False
                 return True
-            elif isinstance(a, np.ndarray):
+            if isinstance(a, dict):
+                for k in set(a.keys()+b.keys()):
+                    if (k not in a) or (k not in b):
+                        return False
+                    ak, bk = a[k], b[k]
+                    if not are_equal(ak, bk):
+                        return False
+                    return True
+            if isinstance(a, np.ndarray):
                 return np.array_equal(a,b)
-            else:
-                return (a==b)
+            return (a==b)
         objects = self.get_attributes(*attr)
         obj0 = objects[0]
         for obj in objects[1:]:
@@ -453,6 +460,12 @@ class DiscreteScalarFieldView(DiscreteScalarFieldViewContainerI, TaggedObjectVie
     def _get_pretty_name(self):
         """Get the name of the discrete field."""
         return self._dfield._pretty_name
+    def _get_latex_name(self):
+        """Get the latex name of the discrete field."""
+        return self._dfield._latex_name
+    def _get_var_name(self):
+        """Get the latex name of the discrete field."""
+        return self._dfield._var_name
     
     def _get_dtype(self):
         """Get the data type of the discrete field."""
@@ -512,14 +525,6 @@ class DiscreteScalarFieldView(DiscreteScalarFieldViewContainerI, TaggedObjectVie
         h ^= hash(self._topology_state)
         return h
 
-    @property
-    def name(self):
-        return self._dfield._name
-
-    @property
-    def pretty_name(self):
-        return self._dfield._pretty_name
-
     @property
     def symbol(self):
         return self._dfield._symbol
@@ -534,6 +539,8 @@ class DiscreteScalarFieldView(DiscreteScalarFieldViewContainerI, TaggedObjectVie
     
     name  = property(_get_name)
     pretty_name  = property(_get_pretty_name)
+    latex_name = property(_get_latex_name)
+    var_name = property(_get_var_name)
     
     dtype = property(_get_dtype)
     initial_values = property(_get_initial_values)
@@ -547,6 +554,7 @@ class DiscreteScalarFieldView(DiscreteScalarFieldViewContainerI, TaggedObjectVie
     memory_request = property(_get_memory_request)
     memory_request_id = property(_get_memory_request_id)
 
+
 class DiscreteScalarField(NamedScalarContainerI, TaggedObject):
     """
     Discrete representation of scalar or vector fields,
@@ -569,7 +577,9 @@ class DiscreteScalarField(NamedScalarContainerI, TaggedObject):
 
     @debug
     def __new__(cls, field, topology, register_discrete_field=True, 
-                name=None, pretty_name=None, **kwds):
+                name=None, pretty_name=None, 
+                var_name=None, latex_name=None,
+                **kwds):
         """
         Creates a discrete field for a given continuous field and topology.
 
@@ -579,6 +589,17 @@ class DiscreteScalarField(NamedScalarContainerI, TaggedObject):
             The continuous field that is dicrerized.
         topology: :class:`~hysop.topology.topology.Topology`
             The topology where to allocate the discrete field.
+        name : string, optional
+            A name for the field.
+        pretty_name: string or unicode, optional.
+            A pretty name used for display whenever possible.
+            Defaults to name.
+        var_name: string, optional.
+            A variable name used for code generation.
+            This will be passed to the symbolic representation of this discrete field.
+        latex_name: string, optional.
+            A variable name used for latex generation.
+            This will be passed to the symbolic representation of this discrete field.
         kwds: dict
             Base class arguments.
         """
@@ -587,12 +608,20 @@ class DiscreteScalarField(NamedScalarContainerI, TaggedObject):
         check_instance(name, str, allow_none=True)
         check_instance(pretty_name, (str,unicode), allow_none=True)
 
-        _name, _pretty_name = cls.format_discrete_names(field.name, 
-                                                        field.pretty_name, topology)
+        _name, _pretty_name, _var_name, _latex_name = \
+                cls.format_discrete_names(field.name, 
+                                          field.pretty_name, 
+                                          field.var_name,
+                                          field.latex_name,
+                                          topology)
+        
         pretty_name = first_not_None(pretty_name, name, _pretty_name)
+        var_name    = first_not_None(var_name,    name, _var_name)
+        latex_name  = first_not_None(latex_name,  name, _latex_name)
         name        = first_not_None(name, _name)
 
         obj = super(DiscreteScalarField, cls).__new__(cls, name=name, pretty_name=pretty_name,
+                                                     var_name=var_name, latex_name=latex_name,
                                                      tag_prefix='df', **kwds)
         assert isinstance(obj, DiscreteScalarFieldView), 'DiscreteScalarFieldView not inherited.'
 
@@ -615,19 +644,23 @@ class DiscreteScalarField(NamedScalarContainerI, TaggedObject):
         return obj
 
     @classmethod
-    def format_discrete_names(cls, name, pretty_name, topology):
+    def format_discrete_names(cls, name, pretty_name, var_name, latex_name, topology):
         from hysop.tools.sympy_utils import subscript
         if (topology is None):
             # Tensor discrete field names (topology is not unique)
             name        = '{}*'.format(name)
             pretty_name = '{}*'.format(pretty_name)
+            latex_name  = '{}'.format(latex_name)
+            var_name    = None
         else:
             # Scalar discrete field names
             name        = '{}_t{}'.format(name, topology.id)
-            pretty_name = '{}_{}{}'.format(pretty_name,
+            pretty_name = '{}.{}{}'.format(pretty_name,
                                             u'\u209c'.encode('utf-8'),
                                             subscript(topology.id).encode('utf-8'))
-        return (name, pretty_name)
+            var_name    = var_name + '_t{}'.format(topology.id)
+            latex_name  = latex_name + '.t_{{{}}}'.format(0)
+        return (name, pretty_name, var_name, latex_name)
 
 
 class DiscreteTensorField(NamedTensorContainerI, DiscreteScalarFieldViewContainerI, TaggedObject):
@@ -649,7 +682,8 @@ class DiscreteTensorField(NamedTensorContainerI, DiscreteScalarFieldViewContaine
     discrete fields may be defined on different topologies.
     """
 
-    def __new__(cls, field, dfields, name=None, pretty_name=None, **kwds):
+    def __new__(cls, field, dfields, name=None, 
+                pretty_name=None, latex_name=None, **kwds):
         check_instance(field, TensorField)
         check_instance(dfields, npw.ndarray, dtype=object, values=DiscreteScalarFieldView)
         assert npw.array_equal(field.shape, dfields.shape)
@@ -661,12 +695,14 @@ class DiscreteTensorField(NamedTensorContainerI, DiscreteScalarFieldViewContaine
                                       name=name, pretty_name=pretty_name,
                                       **kwds)
 
-        _name, _pretty_name = DiscreteScalarField.format_discrete_names(field.name,
-                field.pretty_name, None)
+        _name, _pretty_name, _, _latex_name = DiscreteScalarField.format_discrete_names(
+                field.name, field.pretty_name, None, field.latex_name, None)
         name        = first_not_None(name, _name)
         pretty_name = first_not_None(pretty_name, _pretty_name)
+        latex_name  = first_not_None(latex_name, _latex_name)
 
-        obj = super(DiscreteTensorField, cls).__new__(cls, name=name, pretty_name=pretty_name,
+        obj = super(DiscreteTensorField, cls).__new__(cls, name=name, 
+                pretty_name=pretty_name, latex_name=latex_name,
                 tag_prefix='tdf', tagged_cls=DiscreteTensorField, 
                 contained_objects=dfields, **kwds)
         obj._field   = field
diff --git a/hysop/fields/field_requirements.py b/hysop/fields/field_requirements.py
index 53f6c1379722f6f11958c211f2f28ded4e0e097d..0d40dae6068b272a1ffafd7ab61656c1f7f46941 100644
--- a/hysop/fields/field_requirements.py
+++ b/hysop/fields/field_requirements.py
@@ -202,6 +202,10 @@ class DiscreteFieldRequirements(object):
             assert self.workdim == other.workdim, 'workdim mismatch.'
             assert self.topology_descriptor == other.topology_descriptor, \
                     'topology_descriptor mismatch.'
+            if (self.field.lboundaries != other.field.lboundaries).any():
+                return False
+            if (self.field.rboundaries != other.field.rboundaries).any():
+                return False
             if (other.max_ghosts < self.min_ghosts).any():
                 return False
             if (other.min_ghosts > self.max_ghosts).any():
@@ -229,8 +233,15 @@ class DiscreteFieldRequirements(object):
             msg='{} Dimension mismatch between field and topology.\n field={}d, topology={}d.'
             msg=msg.format(self._header, self.field.dim, topology.domain.dim)
             raise RuntimeError(msg)
+        if (topology.grid_resolution != self.topology_descriptor.grid_resolution).any():
+            msg='{} Grid resolution mismatch between requirement and topology.\n '
+            msg+=' requirement={}\n topology={}'
+            msg=msg.format(self._header,
+                    self.topology_descriptor.grid_resolution,
+                    topology.grid_resolution)
+            raise RuntimeError(msg)
         if (topology.global_resolution != self.topology_descriptor.global_resolution).any():
-            msg='{} Discretisation mismatch between requirement and topology.\n '
+            msg='{} Global resolution mismatch between requirement and topology.\n '
             msg+=' requirement={}\n topology={}'
             msg=msg.format(self._header,
                     self.topology_descriptor.global_resolution,
@@ -378,7 +389,8 @@ class MultiFieldRequirements(object):
                     can_split *= req.can_split
                     unknown_topologies.append(req)
 
-            assert can_split.any()
+            #assert can_split.any() /!\ this is not required anymore 
+            #(ie. 1D poisson operator on 1 process has no splittable axes for example)
 
             for req in unknown_topologies:
                 topo = req.topology_descriptor.choose_or_create_topology(known_topologies,
diff --git a/hysop/fields/ghost_exchangers.py b/hysop/fields/ghost_exchangers.py
index ae370aaf1d0110cbe5c205ba90da7970b25e98a4..321595dbd575bb07732f41f6590684cafd1652ec 100644
--- a/hysop/fields/ghost_exchangers.py
+++ b/hysop/fields/ghost_exchangers.py
@@ -13,6 +13,75 @@ from hysop.core.mpi import MPI
 from hysop.core.mpi.topo_tools import TopoTools
 from hysop.backend.device.opencl import cl, clArray
 from hysop.backend.device.opencl.opencl_kernel_launcher import HostLauncherI
+from hysop.constants import BoundaryCondition
+
+
+class  LocalBoundaryExchanger(object):
+    """
+    Helper class to generate symmetric and antisymmetric local ghost exchangers.
+    
+    This is used for non-periodic boundary conditions:
+        HOMOGENEOUS_DIRICHLET: antisymmetric ghost exchange
+        HOMOGENEOUS_NEUMANN:   symmetric ghost exchange
+    """
+    @classmethod
+    def build_exchanger(cls, shape, direction, H, to_left):
+        shape = to_tuple(shape)
+        assert isinstance(direction, int)
+        assert isinstance(H, tuple)
+
+        ndim = len(shape)
+        S    = shape[direction]
+        G    = (S-1)//2
+        H    = np.asarray(H).copy()
+
+        assert direction < ndim
+        assert S%2==1
+        assert G>0
+        assert H.size==G+1
+
+        def mk_slc(*args, **kwds):
+            if 'extend' in kwds:
+                slices = [np.newaxis]*ndim
+            else:
+                slices = [slice(None,None)]*ndim
+            slices[direction] = slice(*args)
+            return tuple(slices)
+        
+        if to_left:
+            src_slc = mk_slc(G, S, +1)
+            dst_slc = mk_slc(0, G+1, +1)
+            islc    = mk_slc(None, None, None)
+            oslc    = mk_slc(None, None, -1)
+        else:
+            src_slc = mk_slc(0, G+1, +1)
+            dst_slc = mk_slc(G, S, +1)
+            islc    = mk_slc(None, None, -1)
+            oslc    = mk_slc(None, None, None)
+        
+        hslc = mk_slc(0, H.size, 1, extend=True)
+        H = H[hslc]
+        
+        def exchange_ghosts(X):
+            assert (X is not None)
+            assert (X.shape == shape)
+            X[dst_slc][oslc] = (H*X[src_slc][islc])
+        return exchange_ghosts
+
+    @classmethod
+    def build_symmetric_exchanger(cls, shape, direction, to_left):
+        S = (shape[direction]+1)//2
+        H = (1,)*S
+        return cls.build_exchanger(shape=shape, direction=direction,
+                                    H=H, to_left=to_left)
+    
+    @classmethod
+    def build_antisymmetric_exchanger(cls, shape, direction, to_left):
+        S = (shape[direction]+1)//2
+        H = (0,)+(-1,)*(S-1)
+        return cls.build_exchanger(shape=shape, direction=direction,
+                                   H=H, to_left=to_left)
+
 
 class GhostExchangerI(object):
     """Abstract interface for a ghost exchanger."""
@@ -25,6 +94,7 @@ class GhostExchangerI(object):
     def __call__(self, **kwds):
         return self.exchange_ghosts(**kwds)
 
+
 class MultiGhostExchanger(GhostExchangerI):
     """Handle multiple ghost exchangers."""
     def __init__(self, name):
@@ -93,6 +163,7 @@ class MultiGhostExchanger(GhostExchangerI):
         assert (self._launcher is not None)
         return self._launcher
 
+
 class GhostExchanger(GhostExchangerI):
     """Prepare a backend specific ghost exchange, possibly on multiple data."""
     def __init__(self, name, topology, data, 
@@ -126,6 +197,7 @@ class GhostExchanger(GhostExchangerI):
         self.base_mpi_type = base_mpi_type
         self.name = name
 
+
 class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
 
     def __init__(self, name, topology, data,
@@ -145,9 +217,19 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
              P < P > P
                  v
              X   P   X
+
+        Diagonal ghosts are exchanged by chaining exchanges on two
+        or more axes.
         
         If ghost_mask is set to GhostMask.CROSS, diagonal ghosts 
         are set to NAN to ensure they are not used.
+
+        Boundary conditions are hidden in the topology parameter:
+            (PERIODIC/PERIODIC) => standard periodic ghost exchange on the domain boundary
+                                   standard periodic ghost accumulation
+            (XXX/YYY)           => symmetric or antisymmetric ghost exchange
+                                   ghost accumulation is a noop
+            Here XXX and YYY are either HOMOGENEOUS_DIRICHLET or HOMOGENEOUS_NEUMANN.
         """
         ghost_op = first_not_None(ghost_op, GhostOperation.EXCHANGE)
         check_instance(ghost_op, GhostOperation)
@@ -185,6 +267,8 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
                                                               ghost_mask=ghost_mask)
         self.inner_ghosts = mesh.get_local_inner_ghost_slices(ghosts=ghosts,
                                                               ghost_mask=ghost_mask)
+        self.boundary_layers = mesh.get_boundary_layer_slices(ghosts=ghosts,
+                                                              ghost_mask=ghost_mask)
         self.all_inner_ghost_slices = mesh.get_all_local_inner_ghost_slices(ghosts=ghosts)
         self.all_outer_ghost_slices = mesh.get_all_local_outer_ghost_slices(ghosts=ghosts)
         self.dim             = dim
@@ -258,12 +342,29 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
 
                 # COMMON PARAMETERS
                 self.local_exchanges  = []
+                self.local_symmetries = []
                 self.diagonal_ghosts  = []
                 self.has_mpi_exchanges = False
 
                 self.from_buffer = None  # should source data be bufferized ?
                 self.to_buffer   = None  # should target data be bufferized ?
 
+            def setup(self):
+                local_symmetries = []
+                for ls in self.local_symmetries:
+                    (buf, slices, shape, d, to_left, bc) = ls
+                    if (bc is BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                        fn = LocalBoundaryExchanger.build_antisymmetric_exchanger(
+                                shape=shape, direction=d, to_left=to_left)
+                    elif (bc is BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                        fn = LocalBoundaryExchanger.build_symmetric_exchanger(
+                                shape=shape, direction=d, to_left=to_left)
+                    else:
+                        msg='Unknown boundary condition {}.'.format(bc)
+                    local_symmetries.append((fn, buf, slices, shape, d, to_left, bc))
+                self.local_symmetries = local_symmetries
+                return self
+
         def msg_tag(i, local_rank, target_rank, d, direction):
             tag = self.base_tag
             tag += (i+1)*7919
@@ -283,6 +384,12 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
         base_dtype      = self.base_dtype
         base_mpi_type   = self.base_mpi_type
         exchange_method = self.exchange_method
+    
+        mesh                 = self.topology.mesh
+        is_at_left_boundary  = mesh.is_at_left_boundary
+        is_at_right_boundary = mesh.is_at_right_boundary
+        left_boundaries      = mesh.local_lboundaries
+        right_boundaries     = mesh.local_rboundaries
 
         src_data_on_device = (self.kind is not Backend.HOST)
         dst_data_on_device = (self.kind is not Backend.HOST)
@@ -310,19 +417,47 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
             for d in self.directions:
                 if ghosts[d]==0:
                     continue
+                
+                lboundary = left_boundaries[d]
+                rboundary = right_boundaries[d]
+                at_left   = is_at_left_boundary[d]
+                at_right  = is_at_right_boundary[d]
+                lnone     = (lboundary is BoundaryCondition.NONE)
+                rnone     = (rboundary is BoundaryCondition.NONE)
+                lperiodic = (lboundary is BoundaryCondition.PERIODIC)
+                rperiodic = (rboundary is BoundaryCondition.PERIODIC)
+                should_exchange_to_left  = (lnone or lperiodic)
+                should_exchange_to_right = (rnone or rperiodic)
+                assert at_left  ^ lnone
+                assert at_right ^ rnone
+
                 inner_left, inner_right, shape = self.inner_ghosts[d]
                 outer_left, outer_right, shape = self.outer_ghosts[d]
 
+                left_boundary_layer, right_boundary_layer, bl_shape = self.boundary_layers[d]
+                assert (left_boundary_layer  is None) ^ (at_left  and not lperiodic)
+                assert (right_boundary_layer is None) ^ (at_right and not rperiodic)
+                
                 left_rank  = neighbour_ranks[0,d]
                 right_rank = neighbour_ranks[1,d]
 
                 nprocs = proc_shape[d]
                 lp.has_mpi_exchanges |= (nprocs > 1)
+                assert (nprocs==1) or should_exchange_to_left or should_exchange_to_right
+                
+                if not should_exchange_to_left:
+                    lp.local_symmetries.append((buf, left_boundary_layer, bl_shape, d, 1, lboundary))
+                if not should_exchange_to_right:
+                    lp.local_symmetries.append((buf, right_boundary_layer, bl_shape, d, 0, rboundary))
 
                 if (nprocs == 1):
                     # We need to exchange with ourselves (by periodicity)
-                    lp.local_exchanges.append((buf,outer_right,inner_left,shape,d))
-                    lp.local_exchanges.append((buf,outer_left,inner_right,shape,d))
+                    assert (at_left and at_right)
+                    assert (should_exchange_to_left == should_exchange_to_right)
+                    should_exchange = should_exchange_to_left
+                    if should_exchange:
+                        lp.local_exchanges.append((buf,outer_right,inner_left,shape,d))
+                        lp.local_exchanges.append((buf,outer_left,inner_right,shape,d))
                 elif (ghost_op is GhostOperation.EXCHANGE):
                     # SEND DIRECTION IS
                     #   INNER GHOSTS ---> OUTER GHOSTS
@@ -334,14 +469,16 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
                         if nprocs==2:
                             # switch left and right in 2 proc periodic case
                             outer_right, outer_left = outer_left, outer_right
-                        lp.v_send_requests.setdefault(('left',left_rank), [])   \
-                                                  .append(buf[inner_left])
-                        lp.v_send_requests.setdefault(('right',right_rank), []) \
-                                                  .append(buf[inner_right])
-                        lp.v_recv_requests.setdefault(('left',left_rank), [])   \
-                                                  .append(buf[outer_left])
-                        lp.v_recv_requests.setdefault(('right',right_rank), []) \
-                                                  .append(buf[outer_right])
+                        if should_exchange_to_left:
+                            lp.v_send_requests.setdefault(('left',left_rank), [])   \
+                                                      .append(buf[inner_left])
+                            lp.v_recv_requests.setdefault(('left',left_rank), [])   \
+                                                      .append(buf[outer_left])
+                        if should_exchange_to_right:
+                            lp.v_send_requests.setdefault(('right',right_rank), []) \
+                                                      .append(buf[inner_right])
+                            lp.v_recv_requests.setdefault(('right',right_rank), []) \
+                                                      .append(buf[outer_right])
                         lp.from_buffer = True
                         lp.to_buffer   = True
                     elif (exchange_method is ExchangeMethod.NEIGHBOR_ALL_TO_ALL_W):
@@ -350,100 +487,106 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
                         if nprocs==2:
                             # switch left and right in 2 proc periodic case
                             outer_right, outer_left = outer_left, outer_right
-                        lp.w_send_requests.setdefault(('left',left_rank), [])   \
-                                                  .append((buf, inner_left))
-                        lp.w_send_requests.setdefault(('right',right_rank), []) \
-                                                  .append((buf, inner_right))
-                        lp.w_recv_requests.setdefault(('left',left_rank), [])   \
-                                                  .append((buf, outer_left))
-                        lp.w_recv_requests.setdefault(('right',right_rank), []) \
-                                                  .append((buf, outer_right))
+                        if should_exchange_to_left:
+                            lp.w_send_requests.setdefault(('left',left_rank), [])   \
+                                                      .append((buf, inner_left))
+                            lp.w_recv_requests.setdefault(('left',left_rank), [])   \
+                                                      .append((buf, outer_left))
+                        if should_exchange_to_right:
+                            lp.w_send_requests.setdefault(('right',right_rank), []) \
+                                                      .append((buf, inner_right))
+                            lp.w_recv_requests.setdefault(('right',right_rank), []) \
+                                                      .append((buf, outer_right))
                         lp.from_buffer = src_data_on_device
                         lp.to_buffer   = dst_data_on_device
                     elif (exchange_method is ExchangeMethod.ISEND_IRECV):
                         # Exchanges with left neighour
-                        assert (left_rank != local_rank) and (left_rank != -1), left_rank
-
-                        # send inner left to left rank
-                        sendtag  = msg_tag(i, local_rank, left_rank, d, 0)
-                        if src_data_on_device:
-                            tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
-                            send_buf = tmp.handle
-                            mpi_type = base_mpi_type.Create_contiguous(send_buf.size)
-                            mpi_type.Commit()
-                            lp.i_src_buffers += ((tmp,buf,inner_left),)
-                        else:
-                            send_buf = buf.handle
-                            mpi_type = TopoTools.create_subarray(inner_left, buf.shape,
+                        if should_exchange_to_left:
+                            # send inner left to left rank
+                            assert (left_rank != local_rank) and (left_rank != -1), left_rank
+                            sendtag  = msg_tag(i, local_rank, left_rank, d, 0)
+                            if src_data_on_device:
+                                tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
+                                send_buf = tmp.handle
+                                mpi_type = base_mpi_type.Create_contiguous(send_buf.size)
+                                mpi_type.Commit()
+                                lp.i_src_buffers += ((tmp,buf,inner_left),)
+                            else:
+                                send_buf = buf.handle
+                                mpi_type = TopoTools.create_subarray(inner_left, buf.shape,
+                                                                         mpi_type=base_mpi_type)
+                            send_kwds = {'buf':[send_buf, mpi_type],
+                                         'dest':left_rank,
+                                         'tag':sendtag}
+                            lp.isend_kwds.append(send_kwds)
+
+                            # receive outer right from left rank
+                            recvtag = msg_tag(i, left_rank, local_rank, d, 1)
+                            if dst_data_on_device:
+                                tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
+                                recv_buf = tmp.handle
+                                mpi_type = base_mpi_type.Create_contiguous(recv_buf.size)
+                                mpi_type.Commit()
+                                lp.i_dst_buffers += ((tmp,buf,outer_left),)
+                            else:
+                                recv_buf = buf.handle
+                                mpi_type = TopoTools.create_subarray(outer_left, buf.shape,
                                                                      mpi_type=base_mpi_type)
-                        send_kwds = {'buf':[send_buf, mpi_type],
-                                     'dest':left_rank,
-                                     'tag':sendtag}
-                        lp.isend_kwds.append(send_kwds)
-
-                        # receive outer right from left rank
-                        recvtag = msg_tag(i, left_rank, local_rank, d, 1)
-                        if dst_data_on_device:
-                            tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
-                            recv_buf = tmp.handle
-                            mpi_type = base_mpi_type.Create_contiguous(recv_buf.size)
-                            mpi_type.Commit()
-                            lp.i_dst_buffers += ((tmp,buf,outer_left),)
-                        else:
-                            recv_buf = buf.handle
-                            mpi_type = TopoTools.create_subarray(outer_left, buf.shape,
-                                                                 mpi_type=base_mpi_type)
-                        recv_kwds = {'buf':[recv_buf, mpi_type],
-                                     'source':left_rank,
-                                     'tag':recvtag}
-                        lp.irecv_kwds.append(recv_kwds)
-
-                        # Exchanges with right neighour
-                        assert (right_rank != local_rank) and (right_rank != -1)
-
-                        # send inner right to right rank
-                        sendtag = msg_tag(i, local_rank, right_rank, d, 1)
-                        if src_data_on_device:
-                            tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
-                            send_buf = tmp.handle
-                            mpi_type = base_mpi_type.Create_contiguous(send_buf.size)
-                            mpi_type.Commit()
-                            lp.i_src_buffers += ((tmp,buf,inner_right),)
-                        else:
-                            send_buf = buf.handle
-                            mpi_type = TopoTools.create_subarray(inner_right, buf.shape,
-                                                                    mpi_type=base_mpi_type)
-                        send_kwds = {'buf':[send_buf, mpi_type],
-                                     'dest':right_rank,
-                                     'tag':sendtag}
-                        lp.isend_kwds.append(send_kwds)
-
-                        # receive outer left from right rank
-                        recvtag = msg_tag(i, right_rank, local_rank, d, 0)
-                        if dst_data_on_device:
-                            tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
-                            recv_buf = tmp.handle
-                            mpi_type = base_mpi_type.Create_contiguous(recv_buf.size)
-                            mpi_type.Commit()
-                            lp.i_dst_buffers += ((tmp,buf,outer_right),)
-                        else:
-                            recv_buf = buf.handle
-                            mpi_type = TopoTools.create_subarray(outer_right, buf.shape,
-                                                                    mpi_type=base_mpi_type)
-                        recv_kwds = {'buf':[recv_buf, mpi_type],
-                                     'source':right_rank,
-                                     'tag':recvtag}
-                        lp.irecv_kwds.append(recv_kwds)
+                            recv_kwds = {'buf':[recv_buf, mpi_type],
+                                         'source':left_rank,
+                                         'tag':recvtag}
+                            lp.irecv_kwds.append(recv_kwds)
+                        
+                        if should_exchange_to_right:
+                            # Exchanges with right neighour
+                            assert (right_rank != local_rank) and (right_rank != -1)
+
+                            # send inner right to right rank
+                            sendtag = msg_tag(i, local_rank, right_rank, d, 1)
+                            if src_data_on_device:
+                                tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
+                                send_buf = tmp.handle
+                                mpi_type = base_mpi_type.Create_contiguous(send_buf.size)
+                                mpi_type.Commit()
+                                lp.i_src_buffers += ((tmp,buf,inner_right),)
+                            else:
+                                send_buf = buf.handle
+                                mpi_type = TopoTools.create_subarray(inner_right, buf.shape,
+                                                                        mpi_type=base_mpi_type)
+                            send_kwds = {'buf':[send_buf, mpi_type],
+                                         'dest':right_rank,
+                                         'tag':sendtag}
+                            lp.isend_kwds.append(send_kwds)
+
+                            # receive outer left from right rank
+                            recvtag = msg_tag(i, right_rank, local_rank, d, 0)
+                            if dst_data_on_device:
+                                tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
+                                recv_buf = tmp.handle
+                                mpi_type = base_mpi_type.Create_contiguous(recv_buf.size)
+                                mpi_type.Commit()
+                                lp.i_dst_buffers += ((tmp,buf,outer_right),)
+                            else:
+                                recv_buf = buf.handle
+                                mpi_type = TopoTools.create_subarray(outer_right, buf.shape,
+                                                                        mpi_type=base_mpi_type)
+                            recv_kwds = {'buf':[recv_buf, mpi_type],
+                                         'source':right_rank,
+                                         'tag':recvtag}
+                            lp.irecv_kwds.append(recv_kwds)
                         lp.from_buffer = src_data_on_device
                         lp.to_buffer   = dst_data_on_device
                     else:
                         msg='Unknown MPI exchange method {}.'.format(exchange_method)
                         raise NotImplementedError(msg)
                 elif (ghost_op in (GhostOperation.ACCUMULATE,)):
-                    # SEND DIRECTION IS
-                    #    OUTER GHOSTS ---> TMP BUFFER
-                    # OPERATION IS
-                    #    INNER GHOSTS = OP(INNER_GHOSTS, OUTER_GHOSTS)
+                    # FOR PERIODIC OR NONE BOUNDARIES:
+                    #   SEND DIRECTION IS
+                    #      OUTER GHOSTS ---> TMP BUFFER
+                    #   OPERATION IS
+                    #      INNER GHOSTS = OP(INNER_GHOSTS, OUTER_GHOSTS)
+                    # ELSE OPERATION IS
+                    #   OUTER_GHOSTS[...] = 0
                     if (exchange_method is ExchangeMethod.NEIGHBOR_ALL_TO_ALL_V):
                         # Send and receive every buffer at once to neighbours
                         # /!\  we send and receive all data components at once
@@ -452,14 +595,16 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
                         if nprocs==2:
                             # switch left and right in 2 proc periodic case
                             inner_right, inner_left = inner_left, inner_right
-                        lp.v_send_requests.setdefault(('left',left_rank), [])    \
-                                                  .append(buf[outer_left])
-                        lp.v_send_requests.setdefault(('right',right_rank), [])  \
-                                                  .append(buf[outer_right])
-                        lp.v_recv_requests.setdefault(('left',left_rank), [])    \
-                                                  .append(buf[inner_left])
-                        lp.v_recv_requests.setdefault(('right',right_rank), [])  \
-                                                  .append(buf[inner_right])
+                        if should_exchange_to_left:
+                            lp.v_send_requests.setdefault(('left',left_rank), [])    \
+                                                      .append(buf[outer_left])
+                            lp.v_recv_requests.setdefault(('left',left_rank), [])    \
+                                                      .append(buf[inner_left])
+                        if should_exchange_to_right:
+                            lp.v_send_requests.setdefault(('right',right_rank), [])  \
+                                                      .append(buf[outer_right])
+                            lp.v_recv_requests.setdefault(('right',right_rank), [])  \
+                                                      .append(buf[inner_right])
                         lp.from_buffer = True
                         lp.to_buffer   = True
                     elif (exchange_method is ExchangeMethod.NEIGHBOR_ALL_TO_ALL_W):
@@ -470,80 +615,85 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
                             inner_right, inner_left = inner_left, inner_right
                         left_rank  = neighbour_ranks[0,d]
                         right_rank = neighbour_ranks[1,d]
-                        lp.w_send_requests.setdefault(('left',left_rank), [])   \
-                                                  .append((buf, outer_left))
-                        lp.w_send_requests.setdefault(('right',right_rank), []) \
-                                                  .append((buf, outer_right))
-                        lp.w_recv_requests.setdefault(('left',left_rank), [])   \
-                                                  .append((buf, inner_left))
-                        lp.w_recv_requests.setdefault(('right',right_rank), []) \
-                                                  .append((buf, inner_right))
+                        if should_exchange_to_left:
+                            lp.w_send_requests.setdefault(('left',left_rank), [])   \
+                                                      .append((buf, outer_left))
+                            lp.w_recv_requests.setdefault(('left',left_rank), [])   \
+                                                      .append((buf, inner_left))
+                        if should_exchange_to_right:
+                            lp.w_send_requests.setdefault(('right',right_rank), []) \
+                                                      .append((buf, outer_right))
+                            lp.w_recv_requests.setdefault(('right',right_rank), []) \
+                                                      .append((buf, inner_right))
                         lp.from_buffer = src_data_on_device
                         lp.to_buffer   = True
                     elif (exchange_method is ExchangeMethod.ISEND_IRECV):
-                        # Exchanges with left neighour
-                        left_rank = neighbour_ranks[0,d]
-                        assert (left_rank != local_rank) and (left_rank != -1)
-
-                        # send outer left to left rank
-                        sendtag  = msg_tag(i, local_rank, left_rank, d, 0)
-                        if src_data_on_device:
-                            tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
-                            send_buf = tmp.handle
-                            mpi_type = base_mpi_type.Create_contiguous(send_buf.size)
+                        if should_exchange_to_left:
+                            # Exchanges with left neighour
+                            left_rank = neighbour_ranks[0,d]
+                            assert (left_rank != local_rank) and (left_rank != -1)
+
+                            # send outer left to left rank
+                            sendtag  = msg_tag(i, local_rank, left_rank, d, 0)
+                            if src_data_on_device:
+                                tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
+                                send_buf = tmp.handle
+                                mpi_type = base_mpi_type.Create_contiguous(send_buf.size)
+                                mpi_type.Commit()
+                                lp.i_src_buffers += ((tmp,buf,outer_left),)
+                            else:
+                                send_buf = buf.handle
+                                mpi_type = TopoTools.create_subarray(outer_left, buf.shape,
+                                                                     mpi_type=base_mpi_type)
+                            send_kwds = {'buf':[send_buf, mpi_type],
+                                         'dest':left_rank,
+                                         'tag':sendtag}
+                            lp.isend_kwds.append(send_kwds)
+
+                            # receive outer right ghosts data from left rank in a tmp buffer
+                            recvtag = msg_tag(i, left_rank, local_rank, d, 1)
+                            mpi_type = base_mpi_type.Create_contiguous(buf[inner_left].size)
                             mpi_type.Commit()
-                            lp.i_src_buffers += ((tmp,buf,outer_left),)
-                        else:
-                            send_buf = buf.handle
-                            mpi_type = TopoTools.create_subarray(outer_left, buf.shape,
-                                                                 mpi_type=base_mpi_type)
-                        send_kwds = {'buf':[send_buf, mpi_type],
-                                     'dest':left_rank,
-                                     'tag':sendtag}
-                        lp.isend_kwds.append(send_kwds)
-
-                        # receive outer right ghosts data from left rank in a tmp buffer
-                        recvtag = msg_tag(i, left_rank, local_rank, d, 1)
-                        mpi_type = base_mpi_type.Create_contiguous(buf[inner_left].size)
-                        mpi_type.Commit()
-                        tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
-                        recv_kwds = {'buf':[tmp.handle, mpi_type],
-                                     'source':left_rank,
-                                     'tag':recvtag}
-                        lp.irecv_kwds.append(recv_kwds)
-                        lp.i_dst_buffers.append((tmp, buf, inner_left))
-
-                        # Exchanges with right neighour
-                        right_rank = neighbour_ranks[1,d]
-                        assert (right_rank != local_rank) and (right_rank != -1)
-
-                        # send outer right to right rank
-                        sendtag = msg_tag(i, local_rank, right_rank, d, 1)
-                        if src_data_on_device:
                             tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
-                            send_buf = tmp.handle
-                            mpi_type = base_mpi_type.Create_contiguous(send_buf.size)
+                            recv_kwds = {'buf':[tmp.handle, mpi_type],
+                                         'source':left_rank,
+                                         'tag':recvtag}
+                            lp.irecv_kwds.append(recv_kwds)
+                            lp.i_dst_buffers.append((tmp, buf, inner_left))
+
+                        if should_exchange_to_right:
+                            # Exchanges with right neighour
+                            right_rank = neighbour_ranks[1,d]
+                            assert (right_rank != local_rank) and (right_rank != -1)
+
+                            # send outer right to right rank
+                            sendtag = msg_tag(i, local_rank, right_rank, d, 1)
+                            if src_data_on_device:
+                                tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
+                                send_buf = tmp.handle
+                                mpi_type = base_mpi_type.Create_contiguous(send_buf.size)
+                                mpi_type.Commit()
+                                lp.i_src_buffers += ((tmp,buf,outer_right),)
+                            else:
+                                send_buf = buf.handle
+                                mpi_type = TopoTools.create_subarray(outer_right, buf.shape,
+                                                                        mpi_type=base_mpi_type)
+                            send_kwds = {'buf':[send_buf, mpi_type],
+                                         'dest':right_rank,
+                                         'tag':sendtag}
+                            lp.isend_kwds.append(send_kwds)
+
+                            # receive outer left ghosts data from right rank in a tmp buffer
+                            recvtag = msg_tag(i, right_rank, local_rank, d, 0)
+                            mpi_type = base_mpi_type.Create_contiguous(buf[inner_right].size)
                             mpi_type.Commit()
-                            lp.i_src_buffers += ((tmp,buf,outer_right),)
-                        else:
-                            send_buf = buf.handle
-                            mpi_type = TopoTools.create_subarray(outer_right, buf.shape,
-                                                                    mpi_type=base_mpi_type)
-                        send_kwds = {'buf':[send_buf, mpi_type],
-                                     'dest':right_rank,
-                                     'tag':sendtag}
-                        lp.isend_kwds.append(send_kwds)
-
-                        # receive outer left ghosts data from right rank in a tmp buffer
-                        recvtag = msg_tag(i, right_rank, local_rank, d, 0)
-                        mpi_type = base_mpi_type.Create_contiguous(buf[inner_right].size)
-                        mpi_type.Commit()
-                        tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
-                        recv_kwds = {'buf':[tmp.handle, mpi_type],
-                                     'source':right_rank,
-                                     'tag':recvtag}
-                        lp.irecv_kwds.append(recv_kwds)
-                        lp.i_dst_buffers.append((tmp, buf, inner_right))
+                            tmp = self.host_backend.empty(shape=shape, dtype=base_dtype)
+                            recv_kwds = {'buf':[tmp.handle, mpi_type],
+                                         'source':right_rank,
+                                         'tag':recvtag}
+                            lp.irecv_kwds.append(recv_kwds)
+                            lp.i_dst_buffers.append((tmp, buf, inner_right))
+                        
                         lp.from_buffer = src_data_on_device
                         lp.to_buffer   = True
                     else:
@@ -686,7 +836,7 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
             lp.v_send_buffer = send_buffer
             lp.v_recv_buffer = recv_buffer
 
-        return lp
+        return lp.setup()
 
 
     def _build_python_launcher(self):
@@ -699,8 +849,10 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
 
         lp = self._prepare_launcher()
 
-        if ghost_op is GhostOperation.EXCHANGE:
+        if (ghost_op is GhostOperation.EXCHANGE):
             def local_exchanges():
+                for (fn, buf, slices, _, _, _, _) in lp.local_symmetries:
+                    fn(buf[slices])
                 for (buf,outer,inner,shape,direction) in lp.local_exchanges:
                     buf[outer] = buf[inner]
                 for (buf,slc,shape,val) in lp.diagonal_ghosts:
@@ -820,14 +972,21 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
                                                 OpenClKernelListLauncher, HostLauncherI
         from hysop.backend.device.opencl.opencl_copy_kernel_launchers \
                                                             import OpenClCopyBufferRectLauncher
-        class MPIGhostExchangeLauncher(HostLauncherI):
-            def __init__(self, fn):
+        class FunctionLauncher(HostLauncherI):
+            def __init__(self, name, fn):
+                super(FunctionLauncher, self).__init__(name=name)
                 self._fn = fn
             def __call__(self, *args, **kwds):
+                super(FunctionLauncher, self).__call__()
                 return self._fn(*args, **kwds)
+        
+        class MPIGhostExchangeLauncher(FunctionLauncher):
+            def __init__(self, fn):
+                super(MPIGhostExchangeLauncher, self).__init__(name='MPI_Ghost_Exchange_Launcher', fn=fn)
 
         lp = self._prepare_launcher()
-        dim = self.dim
+
+        dim             = self.dim
         ghost_op        = self.ghost_op
         exchange_method = self.exchange_method
         comm            = self.topology.comm
@@ -835,11 +994,14 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
 
         # generate the minimal number of temporary backend buffers
         tmp_buffers = {}
-        def mk_tmp(shape, dtype, color=0):
+        def mk_tmp(shape, dtype, color=0, host=False):
             nbytes = np.prod(shape, dtype=np.int64) * dtype.itemsize
-            key = (nbytes,color)
+            key = (host, nbytes, color)
             if key in tmp_buffers:
                 tmp = tmp_buffers[key].reshape(shape).view(dtype=dtype)
+            elif host:
+                tmp = self.host_backend.empty(shape=shape, dtype=dtype)
+                tmp_buffers[key] = tmp
             else:
                 tmp = self.backend.empty(shape=shape, dtype=dtype)
                 tmp_buffers[key] = tmp
@@ -848,13 +1010,40 @@ class CartesianDiscreteFieldGhostExchanger(GhostExchanger):
         if ghost_op is GhostOperation.EXCHANGE:
             name='local_ghosts_exchanges_{}'.format(self.name)
             local_kl = OpenClKernelListLauncher(name=name)
+            for (fn, buf, slices, shape, direction, to_left, bc) in lp.local_symmetries:
+                # SYMMETRIC OR ANTISYMMETRIC LOCAL EXCHANGE
+                dirlabel = DirectionLabels[dim-direction-1]
+                vname = '{}_{}_{}_{}'.format(self.name, 
+                                          dirlabel,
+                                          'left' if to_left else 'right', 
+                                          str(bc).lower())
+
+                tmp = mk_tmp(shape=shape, dtype=base_dtype, host=True)
+                 
+                k0 = OpenClCopyBufferRectLauncher.from_slices(
+                       varname=vname+'_boundary_layer',
+                       src=buf, src_slices=slices, dst=tmp)
+                
+                k1 = OpenClCopyBufferRectLauncher.from_slices(
+                        varname=vname+'_boundary_layer',
+                        src=tmp, dst=buf, dst_slices=slices)
+
+                def apply_fn(f=fn, X=tmp, k0=k0, k1=k1, **kwds):
+                    evt = k0(**kwds)
+                    evt.wait()
+                    f(X)
+                    evt = k1(**kwds)
+                    return evt
+
+                local_kl += FunctionLauncher(vname+'_apply_on_host', apply_fn)
+
             for i,(buf,outer_slc,inner_slc,shape,direction) in enumerate(lp.local_exchanges):
+                # PERIODIC LOCAL EXCHANGE
                 dirlabel = DirectionLabels[dim-direction-1]
                 vname = '{}_{}_{}'.format(self.name, i, dirlabel)
 
                 # some opencl platforms reject inplace buffer copies 
-                # so we use a tmp buffer
-                # to perform local ghost exchanges
+                # so we use a tmp buffer to perform local ghost exchanges
                 tmp = mk_tmp(shape=shape, dtype=base_dtype)
 
                 # exchange all left inner ghosts to right outer ghosts
diff --git a/hysop/fields/tests/test_cartesian.py b/hysop/fields/tests/test_cartesian.py
index fefbde9adad36a983e8d4fd003d4997e50ec3979..3b8a5ed96122e4ebaca67f72b2d885430b955830 100644
--- a/hysop/fields/tests/test_cartesian.py
+++ b/hysop/fields/tests/test_cartesian.py
@@ -1,14 +1,15 @@
 import os, subprocess, sys, time
 from hysop import __ENABLE_LONG_TESTS__
 from hysop.deps import it, np
-from hysop.constants import Backend, ExchangeMethod, GhostOperation, GhostMask, DirectionLabels
-from hysop.tools.parameters import Discretization
+from hysop.constants import Backend, ExchangeMethod, GhostOperation, \
+                        GhostMask, DirectionLabels, BoundaryCondition
+from hysop.tools.parameters import CartesianDiscretization
 from hysop.tools.numerics import is_integer, is_fp
 from hysop.tools.numpywrappers import npw
 from hysop.domain.box import Box
 from hysop.fields.continuous_field import Field
 from hysop.topology.cartesian_topology import CartesianTopology, CartesianTopologyState
-from hysop.testsenv import iter_clenv, test_context
+from hysop.testsenv import iter_clenv, test_context, domain_boundary_iterator
 from hysop.tools.numerics import is_fp, is_integer
 
 def __random_init(data, coords):
@@ -37,194 +38,444 @@ def __cst_init(cst):
 def test_serial_initialization_1d():
     print
     print 'test_serial_initialization_1d()'
+    dim = 1
     npts = (10,)
     nghosts = (2,)
-    discretization = Discretization(npts, nghosts)
-
-    domain = Box(dim=1)
-    F0 = Field(domain=domain, name='F0', nb_components=1)
-    F1 = Field(domain=domain, name='F1', nb_components=2)
-    F2 = Field(domain=domain, name='F2', shape=(2,2))
-
-    topo0 = CartesianTopology(domain=domain, discretization=discretization,
-                    backend=Backend.HOST)
-    topos = (topo0,) + tuple(CartesianTopology(domain=domain, discretization=discretization,
-        backend=Backend.OPENCL, cl_env=cl_env) for cl_env in iter_clenv())
-
-    for topo in topos:
-        print '  Topo {}::{} | {}'.format(topo.full_tag, topo.backend.kind, topo.backend)
-        dF0 = F0.discretize(topo)
-        dF1 = F1.discretize(topo)
-        dF2 = F2.discretize(topo)
-
-        dF0.initialize(__random_init)
-        dF1.initialize(__random_init)
-        dF2.initialize(__random_init)
-
-        data = dF0.data + dF1.data + dF2.data
-        for i,d in enumerate(data):
-            print '    *buffer {}'.format(i)
-            buf = d.get().handle
-            assert buf.shape==(npts[0]+2*nghosts[0]-1,)
-            assert (buf[nghosts[0]:2*nghosts[0]] == buf[nghosts[0]+npts[0]-1:]).all()
-            assert (buf[:nghosts[0]] == buf[npts[0]-1:nghosts[0]+npts[0]-1]).all()
+    
+    for (lbd, rbd) in domain_boundary_iterator(dim):
+        domain = Box(dim=dim, lboundaries=lbd,
+                              rboundaries=rbd)
+        F0 = Field(domain=domain, name='F0', nb_components=1)
+        F1 = Field(domain=domain, name='F1', nb_components=2)
+        F2 = Field(domain=domain, name='F2', shape=(2,2))
+        print '[{}]'.format(F0.format_boundaries())
+        
+        discretization = CartesianDiscretization(npts, nghosts, 
+                                            lboundaries=F0.lboundaries,
+                                            rboundaries=F0.rboundaries)
+
+        topo0 = CartesianTopology(domain=domain, discretization=discretization,
+                        backend=Backend.HOST)
+        topos = (topo0,) + tuple(CartesianTopology(domain=domain, discretization=discretization,
+            backend=Backend.OPENCL, cl_env=cl_env) for cl_env in iter_clenv())
+
+        assert all(t.mesh.global_lboundaries == t.mesh.local_lboundaries == F0.lboundaries for t in topos)
+        assert all(t.mesh.global_rboundaries == t.mesh.local_rboundaries == F0.rboundaries for t in topos)
+
+        for topo in topos:
+            sys.stdout.write('  {}::{} '.format(topo.full_pretty_tag, topo.backend.kind))
+            sys.stdout.flush()
+
+            dF0 = F0.discretize(topo)
+            dF1 = F1.discretize(topo)
+            dF2 = F2.discretize(topo)
+
+            dfields = dF0.dfields + dF1.dfields + dF2.dfields
+            assert all(d.global_lboundaries == d.local_lboundaries == F0.lboundaries for d in dfields)
+            assert all(d.global_rboundaries == d.local_rboundaries == F0.rboundaries for d in dfields)
+
+            dF0.initialize(__random_init)
+            dF1.initialize(__random_init)
+            dF2.initialize(__random_init)
+
+            data = dF0.data + dF1.data + dF2.data
+
+            Nx, = npts
+            Gx, = nghosts
+            Lx, = tuple(discretization.lboundaries)
+            Rx, = tuple(discretization.rboundaries)
+            try:
+                for i,d in enumerate(data):
+                    b = d.get().handle
+                    assert b.shape==(Nx+2*Gx,)
+                    if (Lx==BoundaryCondition.PERIODIC):
+                        assert (b[Gx:2*Gx] == b[Gx+Nx:]).all(), b
+                    elif (Lx==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                        assert (b[Gx] == 0).all(), b
+                        assert (b[:Gx] == -b[Gx+1:2*Gx+1][::-1]).all(), b
+                    elif (Lx==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                        assert (b[:Gx] == +b[Gx+1:2*Gx+1][::-1]).all(), b
+                    else:
+                        raise NotImplementedError('Unknown boundary condition {}.'.format(Lx))
+                    if (Rx==BoundaryCondition.PERIODIC):
+                        assert (b[:Gx] == b[Nx:Gx+Nx]).all(), b
+                    elif (Rx==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                        assert (b[Nx+Gx-1] == 0).all(), b
+                        assert (b[Nx-1:Nx+Gx-1] == -b[Nx+Gx:][::-1]).all(), b
+                    elif (Rx==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                        assert (b[Nx-1:Nx+Gx-1] == +b[Nx+Gx:][::-1]).all(), b
+                    else:
+                        raise NotImplementedError('Unknown boundary condition {}.'.format(Rx))
+                    sys.stdout.write('.')
+                    sys.stdout.flush()
+            finally:
+                print
+
 
 def test_serial_initialization_2d():
     print
     print 'test_serial_initialization_2d()'
-    npts = (11,13)
-    nghosts = (3,5)
-    discretization = Discretization(npts, nghosts)
-
-    domain = Box(dim=2)
-    F0 = Field(domain=domain, name='F0', nb_components=1)
-    F1 = Field(domain=domain, name='F1', nb_components=2)
-    F2 = Field(domain=domain, name='F2', shape=(2,2))
-
-    topo0 = CartesianTopology(domain=domain, discretization=discretization,
-                    backend=Backend.HOST)
-    topos = (topo0,) + tuple(CartesianTopology(domain=domain, discretization=discretization,
-        backend=Backend.OPENCL, cl_env=cl_env) for cl_env in iter_clenv())
-
-    for topo in topos:
-        print '  Topo {}::{} | {}'.format(topo.full_tag, topo.backend.kind, topo.backend)
-        dF0 = F0.discretize(topo)
-        dF1 = F1.discretize(topo)
-        dF2 = F2.discretize(topo)
-
-        for ghost_mask in GhostMask.all:
-            dF0.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
-            dF1.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
-            dF2.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
-
-            data = dF0.data + dF1.data + dF2.data
-            for (i,d) in enumerate(data):
-                print '    *buffer {}'.format(i)
-                buf = d.get().handle
-                so_x = (slice(0,nghosts[0]),
-                         slice(nghosts[0],nghosts[0]+npts[0]-1),
-                         slice(nghosts[0]+npts[0]-1,None))
-                so_y = (slice(0,nghosts[1]),
-                         slice(nghosts[1],nghosts[1]+npts[1]-1),
-                         slice(nghosts[1]+npts[1]-1,None))
-                si_x = (slice(nghosts[0], 2*nghosts[0]),
-                         slice(2*nghosts[0],npts[0]-1),
-                         slice(npts[0]-1, nghosts[0]+npts[0]-1))
-                si_y = (slice(nghosts[1], 2*nghosts[1]),
-                         slice(2*nghosts[1],npts[1]-1),
-                         slice(npts[1]-1, nghosts[1]+npts[1]-1))
-                assert buf.shape==(npts[0]+2*nghosts[0]-1,npts[1]+2*nghosts[1]-1)
-                assert buf[so_x[0],so_y[0]].shape == nghosts
-                assert buf[so_x[2],so_y[2]].shape == nghosts
-                assert buf[so_x[1],so_y[1]].shape == tuple([_-1 for _ in npts])
-                assert buf[si_x[0],si_y[0]].shape == nghosts
-                assert buf[si_x[2],si_y[2]].shape == nghosts
-                assert np.all(buf[so_x[0],so_y[1]] == buf[si_x[2],so_y[1]])
-                assert np.all(buf[so_x[2],so_y[1]] == buf[si_x[0],so_y[1]])
-                assert np.all(buf[so_x[1],so_y[0]] == buf[so_x[1],si_y[2]])
-                assert np.all(buf[so_x[1],so_y[2]] == buf[so_x[1],si_y[0]])
-                if (ghost_mask is GhostMask.FULL):
-                    assert np.all(buf[so_x[0],so_y[0]]==buf[si_x[2],si_y[2]])
-                    assert np.all(buf[so_x[2],so_y[0]]==buf[si_x[0],si_y[2]])
-                    assert np.all(buf[so_x[2],so_y[2]]==buf[si_x[0],si_y[0]])
-                    assert np.all(buf[so_x[0],so_y[2]]==buf[si_x[2],si_y[0]])
-                elif (ghost_mask is GhostMask.CROSS):
-                    assert np.all(np.isnan(buf[so_x[0],so_y[0]]))
-                    assert np.all(np.isnan(buf[so_x[2],so_y[0]]))
-                    assert np.all(np.isnan(buf[so_x[2],so_y[2]]))
-                    assert np.all(np.isnan(buf[so_x[0],so_y[2]]))
+    dim = 2
+    npts = (4,8)
+    nghosts = (1,2)
+    for (lbd, rbd) in domain_boundary_iterator(dim):
+        domain = Box(dim=dim, lboundaries=lbd,
+                              rboundaries=rbd)
+        F0 = Field(domain=domain, name='F0', nb_components=1)
+        F1 = Field(domain=domain, name='F1', nb_components=2)
+        print '[{}]'.format(F0.format_boundaries())
+        
+        discretization = CartesianDiscretization(npts, nghosts, 
+                                            lboundaries=F0.lboundaries,
+                                            rboundaries=F0.rboundaries)
+
+        topo0 = CartesianTopology(domain=domain, discretization=discretization,
+                        backend=Backend.HOST)
+        topos = (topo0,) + tuple(CartesianTopology(domain=domain, discretization=discretization,
+            backend=Backend.OPENCL, cl_env=cl_env) for cl_env in iter_clenv())
+
+        assert all(np.all(t.mesh.local_lboundaries == F0.lboundaries) for t in topos)
+        assert all(np.all(t.mesh.local_rboundaries == F0.rboundaries) for t in topos)
+
+        for topo in topos:
+            sys.stdout.write('  {}::{}\n'.format(topo.full_pretty_tag, topo.backend.kind))
+            sys.stdout.flush()
+
+            dF0 = F0.discretize(topo)
+            dF1 = F1.discretize(topo)
+
+            dfields = dF0.dfields + dF1.dfields
+            assert all(np.all(d.local_lboundaries == F0.lboundaries) for d in dfields)
+            assert all(np.all(d.local_rboundaries == F0.rboundaries) for d in dfields)
+            
+            Ny,Nx = npts
+            Gy,Gx = nghosts
+            Ly,Lx = tuple(discretization.lboundaries)
+            Ry,Rx = tuple(discretization.rboundaries)
+            Xo = (slice(0,Gx),
+                   slice(Gx,Gx+Nx),
+                   slice(Gx+Nx,None))
+            Yo = (slice(0,Gy),
+                   slice(Gy,Gy+Ny),
+                   slice(Gy+Ny,None))
+            Xi = (slice(Gx, 2*Gx),
+                   slice(2*Gx,Nx),
+                   slice(Nx, Gx+Nx))
+            Yi = (slice(Gy, 2*Gy),
+                   slice(2*Gy,Ny),
+                   slice(Ny, Gy+Ny))
+            Ix = (slice(None,None,+1), slice(None,None,-1))
+            Iy = (slice(None,None,-1), slice(None,None,+1))
+            data = dF0.data + dF1.data
+
+            for ghost_mask in GhostMask.all:
+                dF0.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
+                dF1.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
+
+                sys.stdout.write('    *{:<6} '.format(str(ghost_mask)+':'))
+                sys.stdout.flush()
+
+                if ghost_mask is GhostMask.FULL:
+                    Fx = slice(None,None)
+                    Fy = slice(None,None)
+                elif ghost_mask is GhostMask.CROSS:
+                    # we exclude exterior ghosts because pattern is CROSS
+                    # we exclude interior ghost because of boundary clashes:
+                    # (dirichlet boundary conditions forces 0)
+                    # For the moments zeroes are in the compute domain
+                    # and the grid is fully colocated...
+                    Fx = Xi[1]
+                    Fy = Yi[1]
                 else:
-                    msg='Unknown ghost mask {}.'.format(ghost_mask)
-                    raise NotImplementedError(msg)
+                    raise NotImplementedError(ghost_mask)
+
+                try:
+                    for (i,d) in enumerate(data):
+                        b = d.get().handle
+
+                        assert b.shape==(Ny+2*Gy,Nx+2*Gx)
+                        assert b[Yo[1],Xo[1]].shape == npts
+
+                        assert b[Yo[0],Xo[0]].shape == nghosts
+                        assert b[Yo[0],Xo[2]].shape == nghosts
+                        assert b[Yo[2],Xo[0]].shape == nghosts
+                        assert b[Yo[2],Xo[2]].shape == nghosts
+
+                        assert b[Yi[0],Xi[0]].shape == nghosts
+                        assert b[Yi[0],Xi[2]].shape == nghosts
+                        assert b[Yi[2],Xi[0]].shape == nghosts
+                        assert b[Yi[2],Xi[2]].shape == nghosts
+
+                        if ghost_mask is GhostMask.FULL:
+                            assert b[Fy,Fx].shape == b.shape
+                        elif ghost_mask is GhostMask.CROSS:
+                            assert b[Fy,Fx].shape == (Ny-2*Gy, Nx-2*Gx)
+                        else:
+                            raise NotImplementedError(ghost_mask)
+                        
+                        if (Lx==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Fy,Xo[0]] == b[Fy,Xi[2]]), '\n'+str(d)
+                        elif (Lx==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Fy,Gx] == 0).all(), '\n'+str(d)
+                            assert (b[Fy,:Gx] == -b[Fy,Gx+1:2*Gx+1][Ix]).all(), '\n'+str(d)
+                        elif (Lx==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[Fy,:Gx] == +b[Fy,Gx+1:2*Gx+1][Ix]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Lx))
+                        
+                        if (Rx==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Fy,Xo[2]] == b[Fy,Xi[0]]), '\n'+str(d)
+                        elif (Rx==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Fy,Nx+Gx-1] == 0).all(), '\n'+str(d)
+                            assert (b[Fy,Nx-1:Nx+Gx-1] == -b[Fy,Nx+Gx:][Ix]).all(), '\n'+str(d)
+                        elif (Rx==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[Fy,Nx-1:Nx+Gx-1] == +b[Fy,Nx+Gx:][Ix]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Rx))
+                        
+                        if (Ly==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Yo[0],Fx] == b[Yi[2],Fx]), '\n'+str(d)
+                        elif (Ly==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Gy,Fx] == 0).all(), '\n'+str(d)
+                            assert (b[:Gy,Fx] == -b[Gy+1:2*Gy+1,Fx][Iy]).all(), '\n'+str(d)
+                        elif (Ly==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[:Gy,Fx] == +b[Gy+1:2*Gy+1,Fx][Iy]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Ly))
+                        
+                        if (Ry==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Yo[2],Fx] == b[Yi[0],Fx]), '\n'+str(d)
+                        elif (Ry==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Ny+Gy-1,Fx] == 0).all(), '\n'+str(d)
+                            assert (b[Ny-1:Ny+Gy-1,Fx] == -b[Ny+Gy:,Fx][Iy]).all(), '\n'+str(d)
+                        elif (Ry==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[Ny-1:Ny+Gy-1,Fx] == +b[Ny+Gy:,Fx][Iy]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Ry))
+
+                        if (ghost_mask is GhostMask.FULL):
+                            if (Lx==Ly==Rx==Ry==BoundaryCondition.PERIODIC):
+                                assert np.all(b[Yo[0],Xo[0]]==b[Yi[2],Xi[2]]), '\n'+str(d)
+                                assert np.all(b[Yo[2],Xo[0]]==b[Yi[0],Xi[2]]), '\n'+str(d)
+                                assert np.all(b[Yo[2],Xo[2]]==b[Yi[0],Xi[0]]), '\n'+str(d)
+                                assert np.all(b[Yo[0],Xo[2]]==b[Yi[2],Xi[0]]), '\n'+str(d)
+                        elif (ghost_mask is GhostMask.CROSS):
+                            assert np.all(np.isnan(b[Yo[0],Xo[0]])), '\n'+str(d)
+                            assert np.all(np.isnan(b[Yo[2],Xo[0]])), '\n'+str(d)
+                            assert np.all(np.isnan(b[Yo[2],Xo[2]])), '\n'+str(d)
+                            assert np.all(np.isnan(b[Yo[0],Xo[2]])), '\n'+str(d)
+                        else:
+                            msg='Unknown ghost mask {}.'.format(ghost_mask)
+                            raise NotImplementedError(msg)
+                        sys.stdout.write('.')
+                        sys.stdout.flush()
+                finally:
+                    print
 
 
 def test_serial_initialization_3d():
     print
     print 'test_serial_initialization_3d()'
-    npts = (17,11,15)
-    nghosts = (5,2,3)
-    discretization = Discretization(npts, nghosts)
-
-    domain = Box(dim=3)
-    F0 = Field(domain=domain, name='F0', nb_components=1)
-    F1 = Field(domain=domain, name='F1', nb_components=2)
-    F2 = Field(domain=domain, name='F2', shape=(2,2))
-
-    topo0 = CartesianTopology(domain=domain, discretization=discretization,
-                    backend=Backend.HOST)
-    topos = (topo0,) + tuple(CartesianTopology(domain=domain, discretization=discretization,
-        backend=Backend.OPENCL, cl_env=cl_env) for cl_env in iter_clenv())
-
-    for topo in topos:
-        print '  Topo {}::{} | {}'.format(topo.full_tag, topo.backend.kind, topo.backend)
-        dF0 = F0.discretize(topo)
-        dF1 = F1.discretize(topo)
-        dF2 = F2.discretize(topo)
-        for ghost_mask in GhostMask.all:
-            dF0.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
-            dF1.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
-            dF2.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
-
-            data = dF0.data + dF1.data + dF2.data
-            for (i,d) in enumerate(data):
-                print '    *buffer {}'.format(i)
-                buf = d.get().handle
-                so_x = (slice(0,nghosts[0]),
-                         slice(nghosts[0],nghosts[0]+npts[0]-1),
-                         slice(nghosts[0]+npts[0]-1,None))
-                so_y = (slice(0,nghosts[1]),
-                         slice(nghosts[1],nghosts[1]+npts[1]-1),
-                         slice(nghosts[1]+npts[1]-1,None))
-                so_z = (slice(0,nghosts[2]),
-                         slice(nghosts[2],nghosts[2]+npts[2]-1),
-                         slice(nghosts[2]+npts[2]-1,None))
-                si_x = (slice(nghosts[0], 2*nghosts[0]),
-                         slice(2*nghosts[0],npts[0]-1),
-                         slice(npts[0]-1, nghosts[0]+npts[0]-1))
-                si_y = (slice(nghosts[1], 2*nghosts[1]),
-                         slice(2*nghosts[1],npts[1]-1),
-                         slice(npts[1]-1, nghosts[1]+npts[1]-1))
-                si_z = (slice(nghosts[2], 2*nghosts[2]),
-                         slice(2*nghosts[2],npts[2]-1),
-                         slice(npts[2]-1, nghosts[2]+npts[2]-1))
-                assert buf.shape==(npts[0]+2*nghosts[0]-1,npts[1]+2*nghosts[1]-1,npts[2]
-                                    +2*nghosts[2]-1)
-                assert buf[so_x[0],so_y[0],so_z[0]].shape == nghosts
-                assert buf[so_x[2],so_y[2],so_z[0]].shape == nghosts
-                assert buf[so_x[0],so_y[0],so_z[2]].shape == nghosts
-                assert buf[so_x[2],so_y[2],so_z[2]].shape == nghosts
-                assert buf[so_x[1],so_y[1],so_z[1]].shape == tuple([_-1 for _ in npts])
-                assert buf[si_x[0],si_y[0],si_z[0]].shape == nghosts
-                assert buf[si_x[2],si_y[2],si_z[0]].shape == nghosts
-                assert buf[si_x[0],si_y[0],si_z[2]].shape == nghosts
-                assert buf[si_x[2],si_y[2],si_z[2]].shape == nghosts
-                assert np.all(buf[so_x[0],so_y[1],so_z[1]] == buf[si_x[2],so_y[1],so_z[1]])
-                assert np.all(buf[so_x[2],so_y[1],so_z[1]] == buf[si_x[0],so_y[1],so_z[1]])
-                assert np.all(buf[so_x[1],so_y[0],so_z[1]] == buf[so_x[1],si_y[2],so_z[1]])
-                assert np.all(buf[so_x[1],so_y[2],so_z[1]] == buf[so_x[1],si_y[0],so_z[1]])
-                assert np.all(buf[so_x[1],so_y[1],so_z[0]] == buf[so_x[1],so_y[1],si_z[2]])
-                assert np.all(buf[so_x[1],so_y[1],so_z[2]] == buf[so_x[1],so_y[1],si_z[0]])
-                if (ghost_mask is GhostMask.FULL):
-                    assert np.all(buf[so_x[0],so_y[0],so_z[0]]==buf[si_x[2],si_y[2],si_z[2]])
-                    assert np.all(buf[so_x[2],so_y[0],so_z[0]]==buf[si_x[0],si_y[2],si_z[2]])
-                    assert np.all(buf[so_x[2],so_y[2],so_z[0]]==buf[si_x[0],si_y[0],si_z[2]])
-                    assert np.all(buf[so_x[0],so_y[2],so_z[0]]==buf[si_x[2],si_y[0],si_z[2]])
-                    assert np.all(buf[so_x[0],so_y[0],so_z[2]]==buf[si_x[2],si_y[2],si_z[0]])
-                    assert np.all(buf[so_x[2],so_y[0],so_z[2]]==buf[si_x[0],si_y[2],si_z[0]])
-                    assert np.all(buf[so_x[2],so_y[2],so_z[2]]==buf[si_x[0],si_y[0],si_z[0]])
-                    assert np.all(buf[so_x[0],so_y[2],so_z[2]]==buf[si_x[2],si_y[0],si_z[0]])
-                elif (ghost_mask is GhostMask.CROSS):
-                    assert np.all(np.isnan(buf[so_x[0],so_y[0],so_z[0]]))
-                    assert np.all(np.isnan(buf[so_x[2],so_y[0],so_z[0]]))
-                    assert np.all(np.isnan(buf[so_x[2],so_y[2],so_z[0]]))
-                    assert np.all(np.isnan(buf[so_x[0],so_y[2],so_z[0]]))
-                    assert np.all(np.isnan(buf[so_x[0],so_y[0],so_z[2]]))
-                    assert np.all(np.isnan(buf[so_x[2],so_y[0],so_z[2]]))
-                    assert np.all(np.isnan(buf[so_x[2],so_y[2],so_z[2]]))
-                    assert np.all(np.isnan(buf[so_x[0],so_y[2],so_z[2]]))
+    dim = 3
+    npts = (8,5,5)
+    nghosts = (3,1,2)
+    for (lbd, rbd) in domain_boundary_iterator(dim):
+        domain = Box(dim=dim, lboundaries=lbd,
+                              rboundaries=rbd)
+        F0 = Field(domain=domain, name='F0', nb_components=1)
+        F1 = Field(domain=domain, name='F1', nb_components=3)
+        print '[{}]'.format(F0.format_boundaries())
+        
+        discretization = CartesianDiscretization(npts, nghosts, 
+                                            lboundaries=F0.lboundaries,
+                                            rboundaries=F0.rboundaries)
+
+        topo0 = CartesianTopology(domain=domain, discretization=discretization,
+                        backend=Backend.HOST)
+        topos = (topo0,) + tuple(CartesianTopology(domain=domain, discretization=discretization,
+            backend=Backend.OPENCL, cl_env=cl_env) for cl_env in iter_clenv())
+
+        assert all(np.all(t.mesh.local_lboundaries == F0.lboundaries) for t in topos)
+        assert all(np.all(t.mesh.local_rboundaries == F0.rboundaries) for t in topos)
+
+        for topo in topos:
+            sys.stdout.write('  {}::{}\n'.format(topo.full_pretty_tag, topo.backend.kind))
+            sys.stdout.flush()
+
+            dF0 = F0.discretize(topo)
+            dF1 = F1.discretize(topo)
+
+            dfields = dF0.dfields + dF1.dfields
+            assert all(np.all(d.local_lboundaries == F0.lboundaries) for d in dfields)
+            assert all(np.all(d.local_rboundaries == F0.rboundaries) for d in dfields)
+            
+            Nz,Ny,Nx = npts
+            Gz,Gy,Gx = nghosts
+            Lz,Ly,Lx = tuple(discretization.lboundaries)
+            Rz,Ry,Rx = tuple(discretization.rboundaries)
+            Xo = (slice(0,Gx),
+                   slice(Gx,Gx+Nx),
+                   slice(Gx+Nx,None))
+            Yo = (slice(0,Gy),
+                   slice(Gy,Gy+Ny),
+                   slice(Gy+Ny,None))
+            Zo = (slice(0,Gz),
+                   slice(Gz,Gz+Nz),
+                   slice(Gz+Nz,None))
+            Xi = (slice(Gx, 2*Gx),
+                   slice(2*Gx,Nx),
+                   slice(Nx, Gx+Nx))
+            Yi = (slice(Gy, 2*Gy),
+                   slice(2*Gy,Ny),
+                   slice(Ny, Gy+Ny))
+            Zi = (slice(Gz, 2*Gz),
+                   slice(2*Gz,Nz),
+                   slice(Nz, Gz+Nz))
+            Ix = (slice(None,None,+1), slice(None,None,+1), slice(None,None,-1))
+            Iy = (slice(None,None,+1), slice(None,None,-1), slice(None,None,+1))
+            Iz = (slice(None,None,-1), slice(None,None,+1), slice(None,None,+1))
+            data = dF0.data + dF1.data
+
+            for ghost_mask in GhostMask.all:
+                dF0.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
+                dF1.initialize(__random_init, exchange_kwds=dict(ghost_mask=ghost_mask))
+
+                sys.stdout.write('    *{:<6} '.format(str(ghost_mask)+':'))
+                sys.stdout.flush()
+
+                if ghost_mask is GhostMask.FULL:
+                    Fx = slice(None,None)
+                    Fy = slice(None,None)
+                    Fz = slice(None,None)
+                elif ghost_mask is GhostMask.CROSS:
+                    # we exclude exterior ghosts because pattern is CROSS
+                    # we exclude interior ghost because of boundary clashes:
+                    # (dirichlet boundary conditions forces 0)
+                    # For the moments zeroes are in the compute domain
+                    # and the grid is fully colocated...
+                    Fx = Xi[1]
+                    Fy = Yi[1]
+                    Fz = Zi[1]
                 else:
-                    msg='Unknown ghost mask {}.'.format(ghost_mask)
-                    raise NotImplementedError(msg)
+                    raise NotImplementedError(ghost_mask)
+
+                try:
+                    for (i,d) in enumerate(data):
+                        b = d.get().handle
+                        assert b.shape==(Nz+2*Gz,Ny+2*Gy,Nx+2*Gx)
+                        assert b[Zo[1],Yo[1],Xo[1]].shape == npts
+
+                        assert b[Zo[0],Yo[0],Xo[0]].shape == nghosts
+                        assert b[Zo[0],Yo[0],Xo[2]].shape == nghosts
+                        assert b[Zo[0],Yo[2],Xo[0]].shape == nghosts
+                        assert b[Zo[0],Yo[2],Xo[2]].shape == nghosts
+                        assert b[Zo[2],Yo[0],Xo[0]].shape == nghosts
+                        assert b[Zo[2],Yo[0],Xo[2]].shape == nghosts
+                        assert b[Zo[2],Yo[2],Xo[0]].shape == nghosts
+                        assert b[Zo[2],Yo[2],Xo[2]].shape == nghosts
+
+                        assert b[Zi[0],Yi[0],Xi[0]].shape == nghosts
+                        assert b[Zi[0],Yi[0],Xi[2]].shape == nghosts
+                        assert b[Zi[0],Yi[2],Xi[0]].shape == nghosts
+                        assert b[Zi[0],Yi[2],Xi[2]].shape == nghosts
+                        assert b[Zi[2],Yi[0],Xi[0]].shape == nghosts
+                        assert b[Zi[2],Yi[0],Xi[2]].shape == nghosts
+                        assert b[Zi[2],Yi[2],Xi[0]].shape == nghosts
+                        assert b[Zi[2],Yi[2],Xi[2]].shape == nghosts
+
+                        if ghost_mask is GhostMask.FULL:
+                            assert b[Fz,Fy,Fx].shape == b.shape
+                        elif ghost_mask is GhostMask.CROSS:
+                            assert b[Fz,Fy,Fx].shape == (Nz-2*Gz,Ny-2*Gy, Nx-2*Gx)
+                        else:
+                            raise NotImplementedError(ghost_mask)
+                        
+                        if (Lx==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Fz,Fy,Xo[0]] == b[Fz,Fy,Xi[2]]), '\n'+str(d)
+                        elif (Lx==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Fz,Fy,Gx] == 0).all(), '\n'+str(d)
+                            assert (b[Fz,Fy,:Gx] == -b[Fz,Fy,Gx+1:2*Gx+1][Ix]).all(), '\n'+str(d)
+                        elif (Lx==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[Fz,Fy,:Gx] == +b[Fz,Fy,Gx+1:2*Gx+1][Ix]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Lx))
+                        
+                        if (Rx==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Fz,Fy,Xo[2]] == b[Fz,Fy,Xi[0]]), '\n'+str(d)
+                        elif (Rx==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Fz,Fy,Nx+Gx-1] == 0).all(), '\n'+str(d)
+                            assert (b[Fz,Fy,Nx-1:Nx+Gx-1] == -b[Fz,Fy,Nx+Gx:][Ix]).all(), '\n'+str(d)
+                        elif (Rx==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[Fz,Fy,Nx-1:Nx+Gx-1] == +b[Fz,Fy,Nx+Gx:][Ix]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Rx))
+                        
+                        if (Ly==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Fz,Yo[0],Fx] == b[Fz,Yi[2],Fx]), '\n'+str(d)
+                        elif (Ly==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Fz,Gy,Fx] == 0).all(), '\n'+str(d)
+                            assert (b[Fz,:Gy,Fx] == -b[Fz,Gy+1:2*Gy+1,Fx][Iy]).all(), '\n'+str(d)
+                        elif (Ly==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[Fz,:Gy,Fx] == +b[Fz,Gy+1:2*Gy+1,Fx][Iy]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Ly))
+                        
+                        if (Ry==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Fz,Yo[2],Fx] == b[Fz,Yi[0],Fx]), '\n'+str(d)
+                        elif (Ry==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Fz,Ny+Gy-1,Fx] == 0).all(), '\n'+str(d)
+                            assert (b[Fz,Ny-1:Ny+Gy-1,Fx] == -b[Fz,Ny+Gy:,Fx][Iy]).all(), '\n'+str(d)
+                        elif (Ry==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[Fz,Ny-1:Ny+Gy-1,Fx] == +b[Fz,Ny+Gy:,Fx][Iy]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Ry))
+                        
+                        if (Lz==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Zo[0],Fy,Fx] == b[Zi[2],Fy,Fx]), '\n'+str(d)
+                        elif (Lz==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Gz,Fy,Fx] == 0).all(), '\n'+str(d)
+                            assert (b[:Gz,Fy,Fx] == -b[Gz+1:2*Gz+1,Fy,Fx][Iz]).all(), '\n'+str(d)
+                        elif (Lz==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[:Gz,Fy,Fx] == +b[Gz+1:2*Gz+1,Fy,Fx][Iz]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Lz))
+                        
+                        if (Rz==BoundaryCondition.PERIODIC):
+                            assert np.all(b[Zo[2],Fy,Fx] == b[Zi[0],Fy,Fx]), '\n'+str(d)
+                        elif (Rz==BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+                            assert (b[Nz+Gz-1,Fy,Fx] == 0).all(), '\n'+str(d)
+                            assert (b[Nz-1:Nz+Gz-1,Fy,Fx] == -b[Nz+Gz:,Fy,Fx][Iz]).all(), '\n'+str(d)
+                        elif (Rz==BoundaryCondition.HOMOGENEOUS_NEUMANN):
+                            assert (b[Nz-1:Nz+Gz-1,Fy,Fx] == +b[Nz+Gz:,Fy,Fx][Iz]).all(), '\n'+str(d)
+                        else:
+                            raise NotImplementedError('Unknown boundary condition {}.'.format(Rz))
+
+                        if (ghost_mask is GhostMask.FULL):
+                            if (Lx==Ly==Lz==Rx==Ry==Rz==BoundaryCondition.PERIODIC):
+                                assert np.all(b[Zo[0],Yo[0],Xo[0]]==b[Zi[2],Yi[2],Xi[2]])
+                                assert np.all(b[Zo[2],Yo[0],Xo[0]]==b[Zi[0],Yi[2],Xi[2]])
+                                assert np.all(b[Zo[2],Yo[2],Xo[0]]==b[Zi[0],Yi[0],Xi[2]])
+                                assert np.all(b[Zo[0],Yo[2],Xo[0]]==b[Zi[2],Yi[0],Xi[2]])
+                                assert np.all(b[Zo[0],Yo[0],Xo[2]]==b[Zi[2],Yi[2],Xi[0]])
+                                assert np.all(b[Zo[2],Yo[0],Xo[2]]==b[Zi[0],Yi[2],Xi[0]])
+                                assert np.all(b[Zo[2],Yo[2],Xo[2]]==b[Zi[0],Yi[0],Xi[0]])
+                                assert np.all(b[Zo[0],Yo[2],Xo[2]]==b[Zi[2],Yi[0],Xi[0]])
+                        elif (ghost_mask is GhostMask.CROSS):
+                            assert np.all(np.isnan(b[Zo[0],Yo[0],Xo[0]]))
+                            assert np.all(np.isnan(b[Zo[2],Yo[0],Xo[0]]))
+                            assert np.all(np.isnan(b[Zo[2],Yo[2],Xo[0]]))
+                            assert np.all(np.isnan(b[Zo[0],Yo[2],Xo[0]]))
+                            assert np.all(np.isnan(b[Zo[0],Yo[0],Xo[2]]))
+                            assert np.all(np.isnan(b[Zo[2],Yo[0],Xo[2]]))
+                            assert np.all(np.isnan(b[Zo[2],Yo[2],Xo[2]]))
+                            assert np.all(np.isnan(b[Zo[0],Yo[2],Xo[2]]))
+                        else:
+                            msg='Unknown ghost mask {}.'.format(ghost_mask)
+                            raise NotImplementedError(msg)
+                        sys.stdout.write('.')
+                        sys.stdout.flush()
+                finally:
+                    print
 
 
 def iter_backends():
@@ -232,10 +483,7 @@ def iter_backends():
     for cl_env in iter_clenv():
         yield (Backend.OPENCL, cl_env)
 
-def test_mpi_ghost_exchange(comm=None):
-    if comm is None:
-        from mpi4py import MPI
-        comm = MPI.COMM_WORLD
+def test_mpi_ghost_exchange_periodic(comm):
     rank = comm.Get_rank()
     size = comm.Get_size()
     dtypes = (np.float32, np.float32, np.float64,
@@ -249,14 +497,15 @@ def test_mpi_ghost_exchange(comm=None):
         print '*'*len(msg)
         print msg
         print '*'*len(msg)
-        print 'test_mpi_ghost_exchange()'.format(size)
-    for dim in xrange(1,4+__ENABLE_LONG_TESTS__):
+        print 'test_mpi_ghost_exchange_periodic()'.format(size)
+    for dim in xrange(1,2+__ENABLE_LONG_TESTS__):
         if rank==0:
             print('  >DIM={}'.format(dim))
 
         npts = (53,47,59,23)[:dim]
         nghosts = (2,1,0,3)[:dim]
-        discretization = Discretization(npts, nghosts)
+        discretization = CartesianDiscretization(npts, nghosts,
+                                        default_boundaries=True)
         domain = Box(dim=dim)
 
         for dtype in dtypes[size-1:size]:
@@ -310,11 +559,12 @@ def test_mpi_ghost_exchange(comm=None):
                                 lghosts, rghosts, shape = dF.inner_ghost_slices[d]
                                 _lghosts, _rghosts, shape = dF.outer_ghost_slices[d]
 
-                                for (i,data) in enumerate(dF.data):
-                                    data[lghosts] = ghost_vals(shape, dtype, i,d,rank,0)
-                                    data[rghosts] = ghost_vals(shape, dtype, i,d,rank,1)
-                                    data[_lghosts] = -10
-                                    data[_rghosts] = +10
+                                if (shape is not None):
+                                    for (i,data) in enumerate(dF.data):
+                                            data[lghosts] = ghost_vals(shape, dtype, i,d,rank,0)
+                                            data[_lghosts] = -10
+                                            data[rghosts] = ghost_vals(shape, dtype, i,d,rank,1)
+                                            data[_rghosts] = +10
 
                                 dF.exchange_ghosts(directions=d, exchange_method=exchange_method)
 
@@ -324,29 +574,106 @@ def test_mpi_ghost_exchange(comm=None):
                                     assert right_rank==-1
                                     left_rank, right_rank = rank, rank
 
-                                for (i,data) in enumerate(dF.data):
-                                    ldata = data[lghosts]
-                                    rdata = data[rghosts]
-                                    if (ldata is not None):
+                                if (shape is not None):
+                                    for (i,data) in enumerate(dF.data):
+                                        ldata = data[lghosts]
+                                        rdata = data[rghosts]
+
                                         ldata = np.atleast_1d(ldata.get())
                                         target_vals = ghost_vals(ldata.shape, dtype, i, d,
                                                 left_rank,1)
-                                        assert np.allclose(ldata, target_vals), (rank,
+                                        assert np.allclose(ldata, target_vals), (rank, 
                                                                                     target_vals)
-                                    if (rdata is not None):
                                         rdata = np.atleast_1d(rdata.get())
-                                        target_vals = ghost_vals(rdata.shape, dtype, i, d,
+                                        target_vals = ghost_vals(rdata.shape, dtype, i, d, 
                                                                                 right_rank, 0)
-                                        assert np.allclose(rdata, target_vals), (rank,
-                                                                                    target_vals)
+                                        assert np.allclose(rdata, target_vals), (rank, 
+                                                                                        target_vals)
                             if rank==0:
                                 print
 
 
-def test_mpi_ghost_accumulate(comm=None):
-    if comm is None:
-        from mpi4py import MPI
-        comm = MPI.COMM_WORLD
+def test_mpi_ghost_exchange_runtime(comm):
+    """Just bruteforce all exchange possibilities to see if nothing crashes in 1D and 2D."""
+    rank = comm.Get_rank()
+    size = comm.Get_size()
+    dtype = np.float32
+
+    if rank==0:
+        print
+        msg = '*** COMM_WORLD_SIZE {} ***'.format(size)
+        print
+        print '*'*len(msg)
+        print msg
+        print '*'*len(msg)
+        print 'test_mpi_ghost_exchange_runtime()'.format(size)
+
+    for dim in xrange(1,2+__ENABLE_LONG_TESTS__):
+        if rank==0:
+            sys.stdout.write('>DIM={}\n'.format(dim))
+
+        npts = (17,16,19)[:dim]
+        nghosts = (2,1,3)[:dim]
+        
+        for shape in it.product(xrange(0,size+1), repeat=dim):
+            if np.prod(shape, dtype=np.uint32)!=size:
+                continue
+            if rank==0:
+                sys.stdout.write('  >CART SHAPE: {}\n'.format(shape))
+            
+            for (backend, cl_env) in iter_backends():
+                if rank==0:
+                    sys.stdout.write('    >BACKEND.{:<7} '.format(str(backend)+':'))
+
+                def breakline(i):
+                    if (rank==0) and ((i+1)%63==0):
+                        sys.stdout.write('\n' + ' '*21)
+                        sys.stdout.flush()
+                        return True
+                    return False
+                
+                i=0
+                brk = False
+                try:
+                    for (lbd, rbd) in domain_boundary_iterator(dim):
+                        domain = Box(dim=dim, lboundaries=lbd,
+                                              rboundaries=rbd)
+
+                        F = Field(domain=domain, name='F', nb_components=1, 
+                                    dtype=dtype, _register=False)
+
+                        discretization = CartesianDiscretization(npts, nghosts, 
+                                                lboundaries=F.lboundaries,
+                                                rboundaries=F.rboundaries)
+
+                        topo = CartesianTopology(domain=domain, discretization=discretization,
+                                                   backend=backend, cart_shape=shape, cl_env=cl_env)
+                        assert (topo.proc_shape==shape).all()
+
+                        dF = F.discretize(topo)
+                        for exchange_method in (ExchangeMethod.ISEND_IRECV,
+                                                ExchangeMethod.NEIGHBOR_ALL_TO_ALL_V,
+                                                ExchangeMethod.NEIGHBOR_ALL_TO_ALL_W):
+                                for d in xrange(dim):
+                                    dF.exchange_ghosts(directions=d, exchange_method=exchange_method)
+                                    if rank==0:
+                                        sys.stdout.write('.')
+                                        brk = breakline(i)
+                                        i+=1
+
+                                    dF.accumulate_ghosts(directions=d, exchange_method=exchange_method)
+                                    if rank==0:
+                                        sys.stdout.write('.')
+                                        brk = breakline(i)
+                                        i+=1
+                                        sys.stdout.flush()
+                finally:
+                    if (rank==0):
+                        sys.stdout.write('\n')
+                        sys.stdout.flush()
+
+
+def test_mpi_ghost_accumulate_periodic(comm):
     rank = comm.Get_rank()
     size = comm.Get_size()
     if rank==0:
@@ -355,20 +682,21 @@ def test_mpi_ghost_accumulate(comm=None):
         print '*'*len(msg)
         print msg
         print '*'*len(msg)
-        print 'test_mpi_ghost_accumulate()'.format(size)
+        print 'test_mpi_ghost_accumulate_periodic()'.format(size)
 
     dtypes = (np.float32, np.float32, np.float64,
               np.complex64, np.complex128,
               np.int16,  np.int32,  np.int64,
               np.uint16, np.uint32, np.uint64)
     assert size-1 < len(dtypes)
-    for dim in xrange(1,4+__ENABLE_LONG_TESTS__):
+    for dim in xrange(1,2+__ENABLE_LONG_TESTS__):
         if rank==0:
             print('  >DIM={}'.format(dim))
 
         npts    = (53,57,51,49)[:dim]
         nghosts = (1,3,0,2)[:dim]
-        discretization = Discretization(npts, nghosts)
+        discretization = CartesianDiscretization(npts, nghosts,
+                                        default_boundaries=True)
         domain = Box(dim=dim)
 
         for dtype in dtypes[size-1:size]:
@@ -448,9 +776,11 @@ def test_mpi_ghost_accumulate(comm=None):
                                                 continue
                                             (iview,ishape) = all_inner_ghost_slices[ndirections][directions][displacements]
                                             (oview,oshape) = all_outer_ghost_slices[ndirections][directions][displacements]
-                                            data[oview] = ghost_vals(oshape, dtype, rank, directions,
-                                                                                        displacements, i)
-
+                                            if (oshape is not None):
+                                                assert (ishape is not None)
+                                                data[oview] = ghost_vals(oshape, dtype, rank, directions,
+                                                                                            displacements, i)
+                                    
                                     dF.accumulate_ghosts(directions=directions,
                                                          exchange_method=exchange_method)
 
@@ -460,7 +790,8 @@ def test_mpi_ghost_accumulate(comm=None):
                                             (iview,ishape) = all_inner_ghost_slices[ndirections][directions][displacements]
                                             (oview,oshape) = all_outer_ghost_slices[ndirections][directions][displacements]
 
-                                            if data[iview] is None:
+                                            if (ishape is None):
+                                                assert (oshape is None)
                                                 continue
 
                                             assert np.array_equal(data[iview].shape, ishape)
@@ -497,11 +828,17 @@ if __name__ == '__main__':
     comm = MPI.COMM_WORLD
     size = comm.Get_size()
 
+    from hysop.tools.warning import disable_hysop_warnings
+
     with test_context():
         if (size==1):
             test_serial_initialization_1d()
             test_serial_initialization_2d()
-            test_serial_initialization_3d()
+            if __ENABLE_LONG_TESTS__:
+                test_serial_initialization_3d()
+        
+        disable_hysop_warnings()
+        test_mpi_ghost_exchange_runtime(comm=comm)
+        test_mpi_ghost_exchange_periodic(comm=comm)
+        test_mpi_ghost_accumulate_periodic(comm=comm)
 
-        test_mpi_ghost_exchange(comm=comm)
-        test_mpi_ghost_accumulate(comm=comm)
diff --git a/hysop/fields/tests/test_fields.py b/hysop/fields/tests/test_fields.py
index 3c73eb7b0d4931f121d0cbe7586c6883614f2dc8..86314bb5b2fee90d41c6429238b8044d1566a7e9 100644
--- a/hysop/fields/tests/test_fields.py
+++ b/hysop/fields/tests/test_fields.py
@@ -1,12 +1,16 @@
 import numpy as np
 
-from hysop import Box, CartesianTopology, Discretization
+from hysop import Box, CartesianTopology, CartesianDiscretization
 from hysop.constants import HYSOP_REAL
-from hysop.fields.continuous_field import Field, TensorField
-from hysop.fields.discrete_field import DiscreteField, DiscreteTensorField, DiscreteScalarFieldView
+from hysop.fields.continuous_field import Field, ScalarField,TensorField
+from hysop.fields.discrete_field import DiscreteField,       \
+                                        DiscreteTensorField, \
+                                        DiscreteScalarFieldView
 from hysop.fields.cartesian_discrete_field import CartesianDiscreteField, \
                                                   CartesianDiscreteScalarFieldView
+from hysop.defaults import VelocityField, VorticityField
 from hysop.tools.types import check_instance
+from hysop.testsenv import domain_boundary_iterator
 
 def test_field():
     domain = Box(dim=3)
@@ -19,7 +23,7 @@ def test_field():
     F6 = F0.gradient()
     F7 = F5.field_like('F7')
 
-    D0 = Discretization(resolution=(5,5,5))
+    D0 = CartesianDiscretization(resolution=(5,5,5), default_boundaries=True)
     T0 = CartesianTopology(domain=domain, discretization=D0)
 
     DF0 = F0.discretize(T0)
@@ -162,7 +166,7 @@ def test_tensor_field():
     for f in T0:
         assert f in T0
 
-    D0 = Discretization(resolution=(5,5,5))
+    D0 = CartesianDiscretization(resolution=(5,5,5), default_boundaries=True)
     topo = CartesianTopology(domain=domain, discretization=D0)
 
     DT0 = T0.discretize(topo)
@@ -237,7 +241,6 @@ def test_tensor_field():
     assert DT0.has_unique_compute_resolution()
     assert DT0.has_unique_resolution()
     assert DT0.has_unique_ghosts()
-    assert DT0.has_unique_boundaries()
     assert DT0.has_unique_space_step()
     assert DT0.has_unique_coords()
     assert DT0.has_unique_mesh_coords()
@@ -248,6 +251,15 @@ def test_tensor_field():
     assert DT0.has_unique_axes()
     assert DT0.has_unique_tstate()
     assert DT0.has_unique_memory_order()
+    assert DT0.has_unique_local_boundaries()
+    assert DT0.has_unique_local_lboundaries()
+    assert DT0.has_unique_local_rboundaries()
+    assert DT0.has_unique_global_boundaries()
+    assert DT0.has_unique_global_lboundaries()
+    assert DT0.has_unique_global_rboundaries()
+    assert DT0.has_unique_is_at_boundary()
+    assert DT0.has_unique_is_at_left_boundary()
+    assert DT0.has_unique_is_at_right_boundary()
 
     dfield = DT0[1,0]
     assert DT0.backend == dfield.backend
@@ -261,8 +273,10 @@ def test_tensor_field():
     assert (DT0.compute_resolution == dfield.compute_resolution).all()
     assert (DT0.resolution == dfield.resolution).all()
     assert (DT0.ghosts == dfield.ghosts).all()
-    assert (DT0.boundaries[0] == dfield.boundaries[0]).all()
-    assert (DT0.boundaries[1] == dfield.boundaries[1]).all()
+    assert (DT0.local_boundaries[0] == dfield.local_boundaries[0]).all()
+    assert (DT0.local_boundaries[1] == dfield.local_boundaries[1]).all()
+    assert (DT0.global_boundaries[0] == dfield.global_boundaries[0]).all()
+    assert (DT0.global_boundaries[1] == dfield.global_boundaries[1]).all()
     assert (DT0.space_step == dfield.space_step).all()
     for i in xrange(DT0.dim):
         assert (DT0.coords[i] == dfield.coords[i]).all()
@@ -286,7 +300,6 @@ def test_tensor_field():
     assert DT8.has_unique_compute_resolution()
     assert DT8.has_unique_resolution()
     assert DT8.has_unique_ghosts()
-    assert DT8.has_unique_boundaries()
     assert DT8.has_unique_space_step()
     assert DT8.has_unique_coords()
     assert DT8.has_unique_mesh_coords()
@@ -297,12 +310,58 @@ def test_tensor_field():
     assert DT8.has_unique_axes()
     assert DT8.has_unique_tstate()
     assert DT8.has_unique_memory_order()
+    assert DT8.has_unique_local_boundaries()
+    assert DT8.has_unique_local_lboundaries()
+    assert DT8.has_unique_local_rboundaries()
+    assert DT8.has_unique_global_boundaries()
+    assert DT8.has_unique_global_lboundaries()
+    assert DT8.has_unique_global_rboundaries()
+    assert DT8.has_unique_is_at_boundary()
+    assert DT8.has_unique_is_at_left_boundary()
+    assert DT8.has_unique_is_at_right_boundary()
     try:
         DT8.dtype
         raise RuntimeError
     except AttributeError:
         pass
 
+def test_boundaries():
+    """This test checks that all boundaries are compatible for velocity and vorticity."""
+    for dim in (1,2,):
+        i=0
+        for (lbd, rbd) in domain_boundary_iterator(dim): 
+            domain = Box(dim=dim, lboundaries=lbd, 
+                                  rboundaries=rbd)
+            V = VelocityField(domain)
+            S = ScalarField(name='S0', domain=domain)
+            divV  = V.div()
+            gradV = V.gradient()
+            lapV  = V.laplacian()
+            print
+            print 'DOMAIN BOUNDARIES:'
+            print ' *boundaries=[{}]'.format(domain.format_boundaries())
+            print 'SCALAR BOUNDARIES:'
+            print ' *{} boundaries=[{}]'.format(S.pretty_name, S.format_boundaries())
+            print 'VELOCITY BOUNDARIES:'
+            for Vi in V.fields:
+                print ' *{} boundaries=[{}]'.format(Vi.pretty_name, Vi.format_boundaries())
+            print '{} BOUNDARIES:'.format(divV.pretty_name)
+            print ' *{} boundaries=[{}]'.format(divV.pretty_name, divV.format_boundaries())
+            print '{} BOUNDARIES:'.format(gradV.pretty_name)
+            for gVi in gradV.fields:
+                print ' *{} boundaries=[{}]'.format(gVi.pretty_name, gVi.format_boundaries())
+            print '{} BOUNDARIES:'.format(lapV.pretty_name)
+            for lVi in lapV.fields:
+                print ' *{} boundaries=[{}]'.format(lVi.pretty_name, lVi.format_boundaries())
+            if (dim>1):
+                rotV = V.curl()
+                print '{} (VORTICITY) BOUNDARIES:'.format(rotV.pretty_name)
+                for Wi in rotV.fields:
+                    print ' *{} boundaries=[{}]'.format(Wi.pretty_name, Wi.format_boundaries())
+    
+
+
 if __name__ == '__main__':
-    test_field()
-    test_tensor_field()
+    #test_field()
+    #test_tensor_field()
+    test_boundaries()
diff --git a/hysop/mesh/cartesian_mesh.py b/hysop/mesh/cartesian_mesh.py
index dcac30f9a5a667d1f4715f2e5048c8a9a297b148..2c24349b24a6ee6b20a173b4531b258e2b6c03d6 100644
--- a/hysop/mesh/cartesian_mesh.py
+++ b/hysop/mesh/cartesian_mesh.py
@@ -81,7 +81,7 @@ class CartesianMeshView(MeshView):
     def _get_grid_npoints(self):
         """
         Effective size of the global mesh.
-        Corresponds to np.prod(self.global_resolution - topology.is_periodic).
+        Corresponds to np.prod(self.grid_resolution)
         """
         return prod(self._mesh._grid_resolution)
 
@@ -131,9 +131,21 @@ class CartesianMeshView(MeshView):
     def _get_global_length(self):
         """Physical size of the global physical domain."""
         return self.__get_transposed_mesh_attr('_global_length')
+    def _get_global_lboundaries(self):
+        """Return global domain left boundaries."""
+        return self.__get_transposed_mesh_attr('_global_lboundaries')
+    def _get_global_rboundaries(self):
+        """Return global domain right boundaries."""
+        return self.__get_transposed_mesh_attr('_global_rboundaries')
     def _get_global_boundaries(self):
         """Return global domain boundaries as a tuple of left and right boundaries."""
-        return self.__get_transposed_mesh_attr('_global_boundaries', iterable=True)
+        return (self._get_global_lboundaries(), self._get_global_rboundaries())
+    def _get_periodicity(self):
+        """
+        Get periodicity of the global boundaries.
+        This is not to be confused with the cartesian communicator periodicity.
+        """
+        return (self._get_global_lboundaries()==BoundaryCondition.PERIODIC)
 
     def _get_local_resolution(self):
         """
@@ -228,7 +240,7 @@ class CartesianMeshView(MeshView):
             GhostMask.FULL:   INCLUDES diagonal ghosts
             GhostMask.CROSS:  EXCLUDES diagonal ghosts
         For each direction, a pair of tuples of slices is returned, one for left and one
-        for the right ghosts.
+        for the right ghosts. If direction has no ghosts (lslice, rslice, shape=None) is returned.
         """
         dim = self.dim
         local_start = self.local_start
@@ -261,8 +273,11 @@ class CartesianMeshView(MeshView):
                 gh_slices(j) if j!=i else
                 slice(local_stop[i]-ghosts[i], local_stop[i])
                 for j in xrange(dim) )
-            inner_shape = resolution.copy()
-            inner_shape[i] = ghosts[i]
+            if (ghosts[i] > 0):
+                inner_shape = resolution.copy()
+                inner_shape[i] = ghosts[i]
+            else:
+                inner_shape   = None
             local_inner_ghost_slices.append( (inner_lslices, inner_rslices, inner_shape) )
 
         return tuple(local_inner_ghost_slices)
@@ -275,7 +290,7 @@ class CartesianMeshView(MeshView):
             GhostMask.FULL:   INCLUDES diagonal ghosts
             GhostMask.CROSS:  EXCLUDES diagonal ghosts
         For each direction, a pair of tuples of slices is returned, one for left and one
-        for the right ghosts.
+        for the right ghosts. If direction has no ghosts (lslice, rslice, shape=None) is returned.
         """
         dim = self.dim
         local_start = self.local_start
@@ -308,12 +323,83 @@ class CartesianMeshView(MeshView):
                 gh_slices(j) if j!=i else
                 slice(local_stop[i], local_stop[i]+ghosts[i])
                 for j in xrange(dim) )
-            outer_shape = resolution.copy()
-            outer_shape[i] = ghosts[i]
+            if (ghosts[i] > 0):
+                outer_shape = resolution.copy()
+                outer_shape[i] = ghosts[i]
+            else:
+                outer_shape   = None
             local_outer_ghost_slices.append( (outer_lslices, outer_rslices, outer_shape) )
 
         return tuple(local_outer_ghost_slices)
 
+    def get_boundary_layer_slices(self, ghosts=None, ghost_mask=GhostMask.CROSS):
+        """
+        Return a list of slices defining the ghosts in this arrays a local indices.
+        Those slices corresponds to non periodic boundary layers (ie. inner + outer ghosts).
+        Depending on the ghost_mask parameter, one can include or exclude diagonal ghosts:
+            GhostMask.FULL:   INCLUDES diagonal ghosts
+            GhostMask.CROSS:  EXCLUDES diagonal ghosts
+        For each direction, a pair of tuples of slices is returned, one for left boundary
+        for the right boundary. If the process is not at_left or not at_right or if boundaries
+        are periodic, None is returned.
+        """
+        dim = self.dim
+        local_start = self.local_start
+        local_stop  = self.local_stop
+        compute_resolution = self.compute_resolution
+        local_resolution   = self.local_resolution
+
+        local_lboundaries = self.local_lboundaries
+        local_rboundaries = self.local_rboundaries
+        blacklisted_boundaries = (BoundaryCondition.PERIODIC, BoundaryCondition.NONE)
+
+        ghosts = to_tuple(ghosts or self.ghosts)
+        ghosts = ghosts*dim if len(ghosts)==1 else ghosts
+        assert len(ghosts)==dim
+
+        if (ghost_mask is GhostMask.FULL):
+            gh_slices = lambda j: slice(None)
+            resolution = local_resolution
+        elif (ghost_mask is GhostMask.CROSS):
+            gh_slices = lambda j: slice(ghosts[j], compute_resolution[j]+ghosts[j])
+            resolution = compute_resolution
+        else:
+            msg_mask='Unknown ghost ghost_mask configuration {}.'
+            msg_mask=msg_mask.format(ghost_mask)
+            raise NotImplementedError(msg_mask)
+
+        boundary_layer_slices = []
+        for i in xrange(dim):
+            (lbd, rbd) = local_lboundaries[i], local_rboundaries[i]
+            has_left_layer  = (lbd not in blacklisted_boundaries)
+            has_right_layer = (rbd not in blacklisted_boundaries)
+
+            if has_left_layer:
+                layer_lslices = tuple(
+                    gh_slices(j) if j!=i else
+                    slice(local_start[i]-ghosts[i], local_start[i]+ghosts[i]+1)
+                    for j in xrange(dim) )
+            else:
+                layer_lslices = None
+
+            if has_right_layer:
+                layer_rslices = tuple(
+                    gh_slices(j) if j!=i else
+                    slice(local_stop[i]-ghosts[i]-1, local_stop[i]+ghosts[i])
+                    for j in xrange(dim) )
+            else:
+                layer_rslices = None
+
+            if (has_left_layer or has_right_layer):
+                layer_shape    = resolution.copy()
+                layer_shape[i] = 2*ghosts[i]+1
+            else:
+                layer_shape = None
+
+            boundary_layer_slices.append( (layer_lslices, layer_rslices, layer_shape) )
+
+        return tuple(boundary_layer_slices)
+
     def get_all_local_inner_ghost_slices(self, ghosts=None):
         """
         Return collection of slices and shapes describing all possible
@@ -327,6 +413,7 @@ class CartesianMeshView(MeshView):
                                 -> {directions} (0=first axis, ..., dim-1=last axis)
                                     -> {displacements} (-1=LEFT, 0=CENTER, +1=RIGHT)
                                         -> (view, shape)
+        If one of the direction has no ghosts (lslice, rslice, shape=None) is returned.
         """
         lshape = self.local_resolution
         dim = self.dim
@@ -359,6 +446,10 @@ class CartesianMeshView(MeshView):
                         else:
                             view  += (slice(ghosts[d], lshape[d]-ghosts[d]),)
                             shape += (lshape[d]-2*ghosts[d],)
+                    if any((Si==0) for Si in shape):
+                        shape = None
+                    else:
+                        shape = npw.asarray(shape, dtype=np.int32)
                     views.setdefault(ndirections, {}) \
                          .setdefault(directions,  {}) \
                          .setdefault(displacements, (view,shape))
@@ -410,6 +501,10 @@ class CartesianMeshView(MeshView):
                         else:
                             view  += (slice(ghosts[d], lshape[d]-ghosts[d]),)
                             shape += (lshape[d]-2*ghosts[d],)
+                    if any((Si==0) for Si in shape):
+                        shape = None
+                    else:
+                        shape = npw.asarray(shape, dtype=np.int32)
                     views.setdefault(ndirections, {}) \
                          .setdefault(directions, {})  \
                          .setdefault(displacements, (view,shape))
@@ -499,12 +594,24 @@ class CartesianMeshView(MeshView):
         Physical size of the local physical domain including ghosts.
         """
         return self.__get_transposed_mesh_attr('_local_length')
+    def _get_local_lboundaries(self):
+        """
+        Return local domain left boundaries.
+        Boundaries on the interior of the global domain have value BoundaryCondition.NONE.
+        """
+        return self.__get_transposed_mesh_attr('_local_lboundaries')
+    def _get_local_rboundaries(self):
+        """
+        Return local domain right boundaries.
+        Boundaries on the interior of the global domain have value BoundaryCondition.NONE.
+        """
+        return self.__get_transposed_mesh_attr('_local_rboundaries')
     def _get_local_boundaries(self):
         """
-        Return local subdomain boundaries as a tuple of left and right boundaries.
+        Return local domain boundaries as a tuple of left and right boundaries.
         Boundaries on the interior of the global domain have value BoundaryCondition.NONE.
         """
-        return self.__get_transposed_mesh_attr('_local_boundaries', iterable=True)
+        return (self._get_local_lboundaries(), self._get_local_rboundaries())
 
 
     def _get_compute_resolution(self):
@@ -776,6 +883,9 @@ class CartesianMeshView(MeshView):
     global_end=property(_get_global_end)
     global_length=property(_get_global_length)
     global_boundaries=property(_get_global_boundaries)
+    global_lboundaries=property(_get_global_lboundaries)
+    global_rboundaries=property(_get_global_rboundaries)
+    periodicity=property(_get_periodicity)
 
     local_resolution=property(_get_local_resolution)
     local_npoints=property(_get_local_npoints)
@@ -787,6 +897,8 @@ class CartesianMeshView(MeshView):
     local_origin=property(_get_local_origin)
     local_end=property(_get_local_end)
     local_length=property(_get_local_length)
+    local_lboundaries=property(_get_local_lboundaries)
+    local_rboundaries=property(_get_local_rboundaries)
     local_boundaries=property(_get_local_boundaries)
     local_indices=property(_get_local_indices)
     local_coords=property(_get_local_coords)
@@ -915,20 +1027,28 @@ class CartesianMesh(CartesianMeshView, Mesh):
         dim = domain.dim
 
         discretization = topology._topology._discretization
-        assert (discretization.resolution>=1).all()
+        assert (discretization.grid_resolution>=1).all()
         assert (discretization.ghosts>=0).all()
-        ghosts     = np.asintegerarray(discretization.ghosts)
-        resolution = np.asintegerarray(discretization.resolution).copy()
-        space_step = npw.asrealarray(domain.length / (resolution - 1))
-        del resolution, discretization
-
-        global_resolution = npw.asintegerarray(topology.global_resolution).copy()
-        global_start      = npw.asintegerarray(global_start).copy()
+        ghosts            = np.asintegerarray(discretization.ghosts).copy()
+        grid_resolution   = np.asintegerarray(discretization.grid_resolution).copy()
+        periodicity       = np.asintegerarray(discretization.periodicity).copy()
+        global_resolution = np.asintegerarray(discretization.global_resolution).copy()
+        # /!\ now we add one point on each periodic axes because the user give grid_resolution
+        #     and not global resolution as it used to be.
+        assert all(global_resolution == grid_resolution + periodicity)
+        space_step = npw.asrealarray(domain.length / (grid_resolution + periodicity - 1))
+
+        topo_global_resolution = npw.asintegerarray(topology.global_resolution).copy()
+        topo_grid_resolution = npw.asintegerarray(topology.grid_resolution).copy()
+        global_start = npw.asintegerarray(global_start).copy()
+        assert all(topo_global_resolution == global_resolution)
+        assert all(topo_grid_resolution   == grid_resolution)
         assert global_start.size == dim
         assert (global_start>=0).all()
 
-        # Remove 1 point on each periodic axe because of periodicity
-        grid_resolution = global_resolution - domain.periodicity
+        # global boundaries and local boundaries
+        (global_lboundaries, global_rboundaries) = discretization.boundaries
+        del discretization
 
         # ---
         # Attributes relative to the distributed mesh
@@ -981,21 +1101,20 @@ class CartesianMesh(CartesianMeshView, Mesh):
         is_at_left_boundary  = (proc_coords == 0)
         is_at_right_boundary = (proc_coords == proc_shape-1)
 
-        # global boundaries and local boundaries
-        global_boundaries = domain.boundaries
+        global_lboundaries = global_lboundaries.copy()
+        global_rboundaries = global_rboundaries.copy()
 
-        lboundaries = np.asarray( [bc if at_left  else BoundaryCondition.NONE
-                for (bc, at_left)  in zip(global_boundaries[0], is_at_left_boundary)  ])
-        rboundaries = np.asarray( [bc if at_right else BoundaryCondition.NONE
-                for (bc, at_right) in zip(global_boundaries[1], is_at_right_boundary) ])
-        local_boundaries = (lboundaries, rboundaries)
+        local_lboundaries = np.asarray( [bc if at_left  else BoundaryCondition.NONE
+                for (bc, at_left)  in zip(global_lboundaries, is_at_left_boundary)  ])
+        local_rboundaries = np.asarray( [bc if at_right else BoundaryCondition.NONE
+                for (bc, at_right) in zip(global_rboundaries, is_at_right_boundary) ])
 
         npw.set_readonly(global_resolution, grid_resolution, global_start, global_stop,
                          global_origin, global_end, global_length,
-                         global_boundaries[0], global_boundaries[1],
+                         global_lboundaries, global_rboundaries,
                          local_resolution, local_start, local_stop,
+                         local_lboundaries, local_rboundaries,
                          local_origin, local_end, local_length,
-                         local_boundaries[0], local_boundaries[1],
                          compute_resolution, ghosts,
                          proc_coords, proc_shape,
                          is_at_left_boundary, is_at_right_boundary,
@@ -1014,7 +1133,8 @@ class CartesianMesh(CartesianMeshView, Mesh):
         self._global_origin         = global_origin
         self._global_end            = global_end
         self._global_length         = global_length
-        self._global_boundaries     = global_boundaries
+        self._global_lboundaries    = global_lboundaries
+        self._global_rboundaries    = global_rboundaries
 
         self._local_resolution      = local_resolution
         self._local_start           = local_start
@@ -1022,7 +1142,8 @@ class CartesianMesh(CartesianMeshView, Mesh):
         self._local_origin          = local_origin
         self._local_end             = local_end
         self._local_length          = local_length
-        self._local_boundaries      = local_boundaries
+        self._local_lboundaries     = local_lboundaries
+        self._local_rboundaries     = local_rboundaries
         self._local_indices         = local_indices
         self._local_coords          = local_coords
         self._local_compute_indices = local_compute_indices
@@ -1046,7 +1167,7 @@ class CartesianMesh(CartesianMeshView, Mesh):
         # check variables and properties at the same time
         from hysop.topology.cartesian_topology import CartesianTopologyView
         check_instance(self.topology, CartesianTopologyView)
-        check_instance(self.grid_resolution, np.ndarray, dtype=HYSOP_INTEGER,  shape=(dim,))
+        check_instance(self.grid_resolution,   np.ndarray, dtype=HYSOP_INTEGER,  shape=(dim,))
         check_instance(self.global_resolution, np.ndarray, dtype=HYSOP_INTEGER,  shape=(dim,))
         check_instance(self.global_start,      np.ndarray, dtype=HYSOP_INTEGER,  shape=(dim,))
         check_instance(self.global_stop,       np.ndarray, dtype=HYSOP_INTEGER,  shape=(dim,))
@@ -1060,7 +1181,10 @@ class CartesianMesh(CartesianMeshView, Mesh):
             check_instance(rg, tuple, values=slice, size=dim)
         check_instance(self.global_boundaries, tuple, values=np.ndarray, size=2)
         for i in xrange(2):
-            check_instance(self.global_boundaries[i], np.ndarray, dtype=object, size=dim)
+            check_instance(self.global_boundaries[i], np.ndarray, dtype=object, size=dim,
+                                values=BoundaryCondition)
+        assert np.array_equal(self.global_boundaries[0], self.global_lboundaries)
+        assert np.array_equal(self.global_boundaries[1], self.global_rboundaries)
 
         check_instance(self.local_resolution, np.ndarray, dtype=HYSOP_INTEGER,  shape=(dim,))
         check_instance(self.local_start,      np.ndarray, dtype=HYSOP_INTEGER,  shape=(dim,))
@@ -1070,18 +1194,21 @@ class CartesianMesh(CartesianMeshView, Mesh):
         check_instance(self.local_length,     np.ndarray, dtype=HYSOP_REAL, shape=(dim,))
         check_instance(self.local_compute_slices, tuple, values=slice, size=dim)
         check_instance(self.local_inner_ghost_slices, tuple, values=tuple, size=dim)
-        for lg,rg,sh in self.local_inner_ghost_slices:
+        for (lg,rg,sh) in self.local_inner_ghost_slices:
             check_instance(lg, tuple, values=slice, size=dim)
             check_instance(rg, tuple, values=slice, size=dim)
-            check_instance(sh, np.ndarray, size=dim, dtype=np.int32)
+            check_instance(sh, np.ndarray, dtype=np.int32, shape=(dim,), allow_none=True)
         check_instance(self.local_outer_ghost_slices, tuple, values=tuple, size=dim)
-        for lg,rg,sh in self.local_outer_ghost_slices:
+        for (lg,rg,sh) in self.local_outer_ghost_slices:
             check_instance(lg, tuple, values=slice, size=dim)
             check_instance(rg, tuple, values=slice, size=dim)
-            check_instance(sh, np.ndarray, size=dim, dtype=np.int32)
+            check_instance(sh, np.ndarray, dtype=np.int32, shape=(dim,), allow_none=True)
         check_instance(self.local_boundaries, tuple, values=np.ndarray, size=2)
         for i in xrange(2):
-            check_instance(self.local_boundaries[i], np.ndarray, dtype=object, size=dim)
+            check_instance(self.local_boundaries[i], np.ndarray, dtype=object, size=dim,
+                                values=BoundaryCondition)
+        assert np.array_equal(self.local_boundaries[0], self.local_lboundaries)
+        assert np.array_equal(self.local_boundaries[1], self.local_rboundaries)
 
         check_instance(self.local_indices, tuple, size=dim, values=np.ndarray)
         check_instance(self.local_mesh_indices, tuple, size=dim, values=np.ndarray)
diff --git a/hysop/mesh/cartesian_submesh.py b/hysop/mesh/cartesian_submesh.py
deleted file mode 100644
index 7bc274f6d40ee6aedd0ea8ad1965cd1c1c499f86..0000000000000000000000000000000000000000
--- a/hysop/mesh/cartesian_submesh.py
+++ /dev/null
@@ -1,179 +0,0 @@
-"""To define a restriction of a Mesh
-(like a subset for a domain)
-
-"""
-from hysop.tools.numpywrappers import npw
-from hysop.tools.parameters import Discretization
-import numpy as np
-from hysop.tools.misc import Utils
-
-
-class SubMesh(object):
-    """
-    A subset of a predefined (distributed) mesh.
-    """
-
-    def __init__(self, mesh, substart, subend):
-        """
-        Parameters
-        ----------
-        mesh : :class:`~hysop.domain.mesh.Mesh`
-            the parent mesh
-        substart : list or array of int
-            indices in the global grid of the lowest point of this submesh
-        subend : list or array of int
-            indices of the 'highest' point of this submesh, in the global grid.
-
-        Warning : subend/substart are global values, that do not depend on the
-        mpi distribution of data.
-
-        todo : a proper scheme to clarify all the notations for meshes
-        (global/local start end and so on).
-        """
-        # Note : all variables with 'global' prefix are related
-        # to the global grid, that is the mesh defined on the whole
-        # domain. These variables do not depend on the mpi distribution.
-
-        # ---
-        # Attributes relative to the global mesh
-
-        # parent mesh
-        self.mesh = mesh
-        # dimension of the submesh
-        self._dim = self.mesh.domain.dim
-
-        # Index of the lowest point of the global submesh in the global grid
-        # of its parent
-        self.substart = npw.asdimarray(substart)
-        # Index of the 'highest' point of the global submesh
-        # in the global grid of its parent
-        self.subend = npw.asdimarray(subend)
-
-        # position of the submesh in the global grid of its parent mesh.
-        global_position_in_parent = [slice(substart[d], self.subend[d] + 1)
-                                     for d in xrange(self._dim)]
-        hh = self.mesh.space_step
-        # Coordinates of the lowest point of this submesh
-        self.global_origin = self.substart * hh + self.mesh.domain.origin
-        # Length of this submesh
-        self.global_length = (self.subend - self.substart) * hh
-
-        # Warning : we must not overpass the parent global discretization.
-        gres = self.subend - self.substart + 1
-        # directions where length is 0, i.e. directions 'normal' to
-        # the submesh.
-        self._n_dir = np.where(gres == 1)[0]
-        # discretization of the subset
-        # Warning : at the time, no ghosts on the submesh!
-        self.discretization = Discretization(gres)
-        # Find which part of submesh is on the current process and
-        # find its computational points. Warning:
-        # the indices of computational points must be
-        # relative to the parent mesh local grid!
-        sl = Utils.intersl(global_position_in_parent, self.mesh.position)
-        # Bool to check if this process holds the end (in any direction)
-        # of the domain. Useful for proper integration on this subset.
-        is_last = [False, ] * self._dim
-
-        # Check if a part of the submesh is present on the current proc.
-        self.on_proc = sl is not None
-
-        if self.on_proc:
-            # Is this mesh on the last process in some direction in the
-            # mpi grid of process?
-            is_last = np.asarray([self.subend[d] < sl[d].stop
-                                  for d in xrange(self._dim)])
-            # position of the LOCAL submesh in the global grid
-            # of the parent mesh
-            self.position_in_parent = [s for s in sl]
-            # Indices of the points of the submesh, relative to
-            # the LOCAL array
-            self.compute_index = self.mesh.convert2local(self.position_in_parent)
-
-            # Resolution of the local submesh
-            self.resolution = [self.compute_index[d].stop - self.compute_index[d].start
-                               for d in xrange(self._dim)]
-
-            # Same as self.compute_index but recomputed to be used
-            # for integration on the submesh
-            self.ind4integ = self.mesh.compute_integ_point(is_last,
-                                                           self.compute_index,
-                                                           self._n_dir)
-            start = [self.position_in_parent[d].start - self.substart[d]
-                     for d in xrange(self._dim)]
-            # position of the LOCAL submesh in the global grid
-            # of the submesh (not the grid of the parent mesh!)
-            self.position = [slice(start[d], start[d] + self.resolution[d])
-                             for d in xrange(self._dim)]
-        else:
-            self.position_in_parent = None
-            self.position = None
-            self.compute_index = [slice(0, 0), ] * self._dim
-            self.ind4integ = self.compute_index
-            self.resolution = [0, ] * self._dim
-
-        # Shift between local submesh and local parent mesh.
-        self.local_start = [self.compute_index[d].start for d in xrange(self._dim)]
-
-        # Coordinates of the points of the local submesh
-        self.coords = self.mesh.reduce_coords(self.mesh.coords,
-                                              self.compute_index)
-        self.coords4int = self.mesh.reduce_coords(self.mesh.coords,
-                                                  self.ind4integ)
-
-    def check_boundaries(self):
-        """
-        Special care when some boundaries of the submesh are on the
-        upper boundaries of the parent mesh.
-        Remind that for periodic bc, such a point does not really
-        exists in the parent mesh.
-        """
-        # List of directions which are periodic
-        periodic_dir = self.mesh.domain.i_periodic_boundaries()
-        if len(periodic_dir) > 0:
-            ll = np.where(
-                self.subend ==
-                self.mesh.global_indices(self.mesh.domain.end))[0]
-            return (ll != self._n_dir).all()
-
-        return True
-
-    def global_resolution(self):
-        """return the resolution of the global grid (on the whole
-        domain, whatever the mpi distribution is).
-        """
-        return self.discretization.resolution
-
-    def cx(self):
-        return self.coords[0][:, ...]
-
-    def cy(self):
-        assert self._dim > 1
-        return self.coords[1][0, :, ...]
-
-    def cz(self):
-        assert self._dim > 2
-        return self.coords[2][0, 0, :]
-
-    def chi(self, *args):
-        """
-        indicator function for points inside this submesh.
-        This is only useful when one require the computation
-        of the intersection of a regular subset and a sphere-like
-        subset.
-        See intersection and subtract methods in Subset class.
-
-        param : tuple of coordinates (like topo.mesh.coords)
-
-        returns : an array of boolean (True if inside)
-        """
-        assert len(args) == self._dim
-        if not self.on_proc:
-            return False
-        origin = [self.coords[d].flat[0] for d in xrange(self._dim)]
-        end = [self.coords[d].flat[-1] for d in xrange(self._dim)]
-        c1 = [np.logical_and(args[d] >= origin[d],
-                             args[d] <= end[d]) for d in xrange(self._dim)]
-        for i in xrange(self._dim - 1):
-            c1[i + 1] = np.logical_and(c1[i], c1[i + 1])
-        return c1[-1]
diff --git a/hysop/mesh/submesh.py b/hysop/mesh/submesh.py
deleted file mode 100644
index 7bc274f6d40ee6aedd0ea8ad1965cd1c1c499f86..0000000000000000000000000000000000000000
--- a/hysop/mesh/submesh.py
+++ /dev/null
@@ -1,179 +0,0 @@
-"""To define a restriction of a Mesh
-(like a subset for a domain)
-
-"""
-from hysop.tools.numpywrappers import npw
-from hysop.tools.parameters import Discretization
-import numpy as np
-from hysop.tools.misc import Utils
-
-
-class SubMesh(object):
-    """
-    A subset of a predefined (distributed) mesh.
-    """
-
-    def __init__(self, mesh, substart, subend):
-        """
-        Parameters
-        ----------
-        mesh : :class:`~hysop.domain.mesh.Mesh`
-            the parent mesh
-        substart : list or array of int
-            indices in the global grid of the lowest point of this submesh
-        subend : list or array of int
-            indices of the 'highest' point of this submesh, in the global grid.
-
-        Warning : subend/substart are global values, that do not depend on the
-        mpi distribution of data.
-
-        todo : a proper scheme to clarify all the notations for meshes
-        (global/local start end and so on).
-        """
-        # Note : all variables with 'global' prefix are related
-        # to the global grid, that is the mesh defined on the whole
-        # domain. These variables do not depend on the mpi distribution.
-
-        # ---
-        # Attributes relative to the global mesh
-
-        # parent mesh
-        self.mesh = mesh
-        # dimension of the submesh
-        self._dim = self.mesh.domain.dim
-
-        # Index of the lowest point of the global submesh in the global grid
-        # of its parent
-        self.substart = npw.asdimarray(substart)
-        # Index of the 'highest' point of the global submesh
-        # in the global grid of its parent
-        self.subend = npw.asdimarray(subend)
-
-        # position of the submesh in the global grid of its parent mesh.
-        global_position_in_parent = [slice(substart[d], self.subend[d] + 1)
-                                     for d in xrange(self._dim)]
-        hh = self.mesh.space_step
-        # Coordinates of the lowest point of this submesh
-        self.global_origin = self.substart * hh + self.mesh.domain.origin
-        # Length of this submesh
-        self.global_length = (self.subend - self.substart) * hh
-
-        # Warning : we must not overpass the parent global discretization.
-        gres = self.subend - self.substart + 1
-        # directions where length is 0, i.e. directions 'normal' to
-        # the submesh.
-        self._n_dir = np.where(gres == 1)[0]
-        # discretization of the subset
-        # Warning : at the time, no ghosts on the submesh!
-        self.discretization = Discretization(gres)
-        # Find which part of submesh is on the current process and
-        # find its computational points. Warning:
-        # the indices of computational points must be
-        # relative to the parent mesh local grid!
-        sl = Utils.intersl(global_position_in_parent, self.mesh.position)
-        # Bool to check if this process holds the end (in any direction)
-        # of the domain. Useful for proper integration on this subset.
-        is_last = [False, ] * self._dim
-
-        # Check if a part of the submesh is present on the current proc.
-        self.on_proc = sl is not None
-
-        if self.on_proc:
-            # Is this mesh on the last process in some direction in the
-            # mpi grid of process?
-            is_last = np.asarray([self.subend[d] < sl[d].stop
-                                  for d in xrange(self._dim)])
-            # position of the LOCAL submesh in the global grid
-            # of the parent mesh
-            self.position_in_parent = [s for s in sl]
-            # Indices of the points of the submesh, relative to
-            # the LOCAL array
-            self.compute_index = self.mesh.convert2local(self.position_in_parent)
-
-            # Resolution of the local submesh
-            self.resolution = [self.compute_index[d].stop - self.compute_index[d].start
-                               for d in xrange(self._dim)]
-
-            # Same as self.compute_index but recomputed to be used
-            # for integration on the submesh
-            self.ind4integ = self.mesh.compute_integ_point(is_last,
-                                                           self.compute_index,
-                                                           self._n_dir)
-            start = [self.position_in_parent[d].start - self.substart[d]
-                     for d in xrange(self._dim)]
-            # position of the LOCAL submesh in the global grid
-            # of the submesh (not the grid of the parent mesh!)
-            self.position = [slice(start[d], start[d] + self.resolution[d])
-                             for d in xrange(self._dim)]
-        else:
-            self.position_in_parent = None
-            self.position = None
-            self.compute_index = [slice(0, 0), ] * self._dim
-            self.ind4integ = self.compute_index
-            self.resolution = [0, ] * self._dim
-
-        # Shift between local submesh and local parent mesh.
-        self.local_start = [self.compute_index[d].start for d in xrange(self._dim)]
-
-        # Coordinates of the points of the local submesh
-        self.coords = self.mesh.reduce_coords(self.mesh.coords,
-                                              self.compute_index)
-        self.coords4int = self.mesh.reduce_coords(self.mesh.coords,
-                                                  self.ind4integ)
-
-    def check_boundaries(self):
-        """
-        Special care when some boundaries of the submesh are on the
-        upper boundaries of the parent mesh.
-        Remind that for periodic bc, such a point does not really
-        exists in the parent mesh.
-        """
-        # List of directions which are periodic
-        periodic_dir = self.mesh.domain.i_periodic_boundaries()
-        if len(periodic_dir) > 0:
-            ll = np.where(
-                self.subend ==
-                self.mesh.global_indices(self.mesh.domain.end))[0]
-            return (ll != self._n_dir).all()
-
-        return True
-
-    def global_resolution(self):
-        """return the resolution of the global grid (on the whole
-        domain, whatever the mpi distribution is).
-        """
-        return self.discretization.resolution
-
-    def cx(self):
-        return self.coords[0][:, ...]
-
-    def cy(self):
-        assert self._dim > 1
-        return self.coords[1][0, :, ...]
-
-    def cz(self):
-        assert self._dim > 2
-        return self.coords[2][0, 0, :]
-
-    def chi(self, *args):
-        """
-        indicator function for points inside this submesh.
-        This is only useful when one require the computation
-        of the intersection of a regular subset and a sphere-like
-        subset.
-        See intersection and subtract methods in Subset class.
-
-        param : tuple of coordinates (like topo.mesh.coords)
-
-        returns : an array of boolean (True if inside)
-        """
-        assert len(args) == self._dim
-        if not self.on_proc:
-            return False
-        origin = [self.coords[d].flat[0] for d in xrange(self._dim)]
-        end = [self.coords[d].flat[-1] for d in xrange(self._dim)]
-        c1 = [np.logical_and(args[d] >= origin[d],
-                             args[d] <= end[d]) for d in xrange(self._dim)]
-        for i in xrange(self._dim - 1):
-            c1[i + 1] = np.logical_and(c1[i], c1[i + 1])
-        return c1[-1]
diff --git a/hysop/mesh/subsets.py b/hysop/mesh/subsets.py
deleted file mode 100644
index 413549730ff7cb38d06140c4dd7cf0895115ef62..0000000000000000000000000000000000000000
--- a/hysop/mesh/subsets.py
+++ /dev/null
@@ -1,889 +0,0 @@
-"""Subsets of a given domain:
-
-* :class:`~hysop.domain.subsets.Sphere`,
-* :class:`~hysop.domain.subsets.HemiSphere`,
-* :class:`~hysop.domain.subsets.Cylinder`,
-* :class:`~hysop.domain.subsets.HemiCylinder`,
-* :class:`~hysop.domain.subsets.SubBox`,
-* :class:`~hysop.domain.subsets.Subset` (abstract base class).
-
-See also
---------
-
-* :class:`~hysop.domain.porous.Porous` for porous (multi-layers) subsets.
-* :ref:`subsets` in HySoP user guide.
-
-"""
-from hysop.domain.domain import Domain
-import numpy as np
-from hysop.topology.cartesian_topology import CartesianTopology
-from hysop.fields.discrete_field import DiscreteField
-from hysop.tools.numpywrappers import npw
-from hysop.fields.continuous_field import Field
-from hysop.mesh.submesh import SubMesh
-import numpy.linalg as la
-from hysop.core.mpi import MPI
-
-
-class Subset(object):
-    """
-    A subset is a geometry defined inside a domain.
-    Given a topology on the parent domain, the subset must
-    provide some lists of indices to allow the computation of a discrete
-    field on the subset, with something like
-
-    data[subset.ind[topo] = ...
-    or
-    data[subset.tab] = ...
-
-    There are two types of subsets:
-    - 'regular' ones, those on which a regular mesh can be defined
-    - others, like spheres, cylinders ...
-
-    Subsets have an attribute 'ind' which is a dictionnary of tuples
-    representing the points inside the subset, (keys = topology) such that:
-    ind[topo] = (i_x, i_y, i_z),
-    with i_x[i], i_y[i], i_z[i] for each index i being the indices in the
-    local grid of a point inside the subset.
-
-    It means that for any discrete field df,
-    df[ind[topo]] represents the grid values of dd inside the subset.
-
-    """
-
-    _TOLCOEF_ = 0.
-
-    def __init__(self, parent, chi=None, group=None):
-        """
-        Parameters
-        ----------
-
-        parent : :class:`~hysop.domain.box.Box`
-            the domain in which the subset is defined
-        func : a python function
-            indicator function of the domain.
-        group : a list of Subsets, optional
-            useful to build a new subset from the union or
-            intersection of subsets.
-
-        Attributes
-        ----------
-        ind : dictionnary
-            indices of points inside the subset, for a given topology.
-            Keys = topology, values = indices, as tuple or arrays.
-
-        Notes
-        -----
-
-        func argument list must start with space coordinates,
-        e.g. for a 3D domain something like::
-
-            def myfunc(x, y, z, ...):
-                ...
-
-        """
-        assert isinstance(parent, Domain)
-        self._parent = parent
-        # dictionnary of indices of points inside the subsets.
-        # Keys = topology, Values = tuple of arrays.
-        self.ind = {}
-        # Dictionnary (key = topo), on_proc[topo] = True
-        # if the subset has points on the current mpi process.
-        self.on_proc = {}
-        # indicator function of the subset
-        self._is_inside = chi
-        self.is_porous = False
-        # list of space direction, used for integration.
-        self.t_dir = [d for d in xrange(parent.dimension)]
-        # dict of mpi communicators, keys=topo
-        # such that self.subcomm[topo] represents the mpi processes
-        # where on_proc[topo] is true. Useful to reduce collective comm
-        # on the subset
-        self.subcomm = {}
-        self.max_subcoords = {}
-        # a list of subsets used to build this subset, from union
-        # or intersection
-        self._group = group
-
-    def chi(self, topo, *args):
-        """Indicator function of the subset
-
-        Returns
-        -------
-        array of bool
-
-        """
-        tol = la.norm(topo.mesh.space_step) * self._TOLCOEF_
-        return self._is_inside(*args) <= tol
-
-    def _set_chi(self, chi):
-        """Reset indicator function
-
-        Mostly for internal setup (during porous obst. init), so do not
-        use it or use it with care ...
-        """
-        self._is_inside = chi
-
-    def get_chi(self):
-        """Get indicator function
-
-        Mostly for internal setup (during porous obst. init), so do not
-        use it or use it with care ...
-        """
-        return self._is_inside
-
-    def discretize(self, topo):
-        """
-        Create the list of indices for points inside the subset
-        for a given topo.
-
-        :param topo: :class:`hysop.topology.topology.CartesianTopology`
-
-        Returns
-        -------
-        tuple of indices
-           indices of points inside the domain, np.where result.
-
-        """
-        assert isinstance(topo, CartesianTopology)
-        if topo not in self.ind:
-            self.ind[topo] = \
-                [np.where(self.chi(topo, *topo.mesh.compute_coords))]
-            self.ind[topo][0] = topo.mesh.local_shift(self.ind[topo][0])
-            self._reduce_topology(topo)
-
-        return self.ind[topo]
-
-    def discretize_group(self, topo, union=True):
-        """Build a subset from a group of subsets
-        Parameters
-        ----------
-        topo : :class:`hysop.topology.topology.CartesianTopology`
-        union : bool, optional
-            compute union of subsets if True, else intersection
-        """
-        if union:
-            self.ind[topo] = [self.union(self._group, topo)]
-        else:
-            self.ind[topo] = [self.intersection(self._group, topo)]
-        self._reduce_topology(topo)
-        return self.ind[topo]
-
-    def _reduce_topology(self, topo):
-        """Find the reduced mpi communicator that handles
-        all points of this subset.
-        """
-        dim = self._parent.dimension
-        self.on_proc[topo] = (np.asarray([len(self.ind[topo][0][i])
-                                          for i in xrange(dim)])
-                              != 0).all()
-
-        plist = np.asarray(topo.comm.allgather(self.on_proc[topo]),
-                           dtype=np.bool)
-        gtopo = topo.comm.Get_group()
-        rks = np.where(plist)[0]
-        subgroup = gtopo.Incl(rks)
-        self.subcomm[topo] = topo.comm.Create(subgroup)
-        self.max_subcoords[topo] = None
-        if self.on_proc[topo]:
-            self.max_subcoords[topo] = topo.proc_coords.copy()
-            self.subcomm[topo].Allreduce(topo.proc_coords.handle(),
-                                         self.max_subcoords[topo].handle(),
-                                         op=MPI.MAX)
-
-    @staticmethod
-    def intersection(slist, topo):
-        """Compute the intersection of subsets
-
-        Parameters
-        ----------
-        slist : a list of :class:`~hysop.domain.subsets.Subset`
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-
-        Returns
-        -------
-        list of tuples
-            the intersection of the subsets in slist
-
-        """
-        c0 = Subset.intersection_as_bool(slist, topo)
-        return topo.mesh.local_shift(np.where(c0))
-
-    @staticmethod
-    def intersection_as_bool(slist, topo):
-        """Compute the intersection of subsets
-
-        Parameters
-        ----------
-        slist : a list of :class:`~hysop.domain.subsets.Subset`
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-
-        Returns
-        -------
-        numpy array of boolean
-            the intersection of the subsets in slist
-
-        """
-        sref = slist[0]
-        ## if len(slist) == 1:
-        ##     return sref.ind[topo]
-        coords = topo.mesh.compute_coords
-        c0 = sref.chi(topo, *coords)
-        for s in slist[1:]:
-            c1 = s.chi(topo, *coords)
-            c0 = np.logical_and(c0, c1)
-        return c0
-
-    @staticmethod
-    def intersection_of_list_of_sets(s1, s2, topo):
-        """Compute the intersection of two lists, each list
-        being the union of a set of subsets.
-
-        Parameters
-        ----------
-        s1, s2 : lists of :class:`~hysop.domain.subsets.Subset`
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-
-        Returns
-        -------
-        numpy array of boolean
-            the intersection of the subsets in slist
-        """
-        c1 = Subset.union_as_bool(s1, topo)
-        c2 = Subset.union_as_bool(s2, topo)
-        return topo.mesh.local_shift(np.where(np.logical_and(c1, c2)))
-
-    @staticmethod
-    def union(slist, topo):
-        """Union of subsets
-
-        Parameters
-        ----------
-        slist : list of :class:`~hysop.domain.subsets.Subset`
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-
-        Returns
-        --------
-        the union of a set of subsets for a
-        given topo as a tuple of arrays which gives
-        the indexes of points inside the union.
-        """
-        return topo.mesh.local_shift(
-            np.where(Subset.union_as_bool(slist, topo)))
-
-    @staticmethod
-    def union_as_bool(slist, topo):
-        """Union of subsets
-
-        Parameters
-        ----------
-        slist : list of :class:`~hysop.domain.subsets.Subset`
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-
-        Returns
-        --------
-        the union of a set of subsets for a
-        given topo as an array of bool, True
-        for points inside the union.
-        """
-        sref = slist[0]
-        coords = topo.mesh.compute_coords
-        c0 = sref.chi(topo, *coords)
-        if len(slist) == 1:
-            return c0
-
-        for s in slist[1:]:
-            c1 = s.chi(topo, *coords)
-            c0 = np.logical_or(c0, c1)
-        return c0
-
-    @staticmethod
-    def subtract(s1, s2, topo):
-        """Difference of subsets
-
-        Parameters
-        ----------
-        s1, s2 : :class:`~hysop.domain.subsets.Subset`
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-
-        Returns
-        --------
-        points in s1 - s2 as a tuple of arrays of indices.
-        """
-        return topo.mesh.local_shift(
-            np.where(Subset.subtract_as_bool(s1, s2, topo)))
-
-    @staticmethod
-    def subtract_as_bool(s1, s2, topo):
-        """Difference of subsets
-
-        Parameters
-        ----------
-        s1, s2 : :class:`~hysop.domain.subsets.Subset`
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-
-        Returns
-        --------
-        points in s1 - s2 as an array of boolean
-        """
-        coords = topo.mesh.compute_coords
-        c1 = s1.chi(topo, *coords)
-        c2 = np.logical_not(s2.chi(topo, *coords))
-        return np.logical_and(c1, c2)
-
-    @staticmethod
-    def subtract_list_of_sets(s1, s2, topo):
-        """Difference of subsets
-
-        Parameters
-        ----------
-        s1, s2 : list of :class:`~hysop.domain.subsets.Subset`
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-
-        Returns
-        --------
-        points in s1 - s2 as a tuple of arrays of indices.
-        """
-        c1 = Subset.union_as_bool(s1, topo)
-        c2 = np.logical_not(Subset.union_as_bool(s2, topo))
-        return topo.mesh.local_shift(np.where(np.logical_and(c1, c2)))
-
-    def integrate_field_allc(self, field, topo, root=None):
-        """Field integration
-
-        Parameters
-        ----------
-        field : :class:`~hysop.fields.continuous_field.Field`
-            a field to be integrated on the box
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-            set mesh/topology used for integration
-        root : int, optional
-            rank of the leading mpi process (to collect data)
-            If None reduction is done on all processes from topo.
-
-        Returns
-        --------
-        a numpy array, with res[i] = integral of component i
-        of the input field over the current subset.
-        """
-        res  = npw.zeros(field.nb_components)
-        gres = npw.zeros(field.nb_components)
-        for i in xrange(res.size):
-            res[i] = self.integrate_field_on_proc(field, topo, component=i)
-        if root is None:
-            topo.comm.Allreduce(res.handle(), gres.handle())
-        else:
-            topo.comm.Reduce(res.handle(), gres.handle(), root=root)
-
-        return gres
-
-    def integrate_field(self, field, topo, component=0, root=None):
-        """Field integration
-
-        Parameters
-        ----------
-        field : :class:`~hysop.fields.continuous_field.Field`
-            a field to be integrated on the box
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-            set mesh/topology used for integration
-        component : int, optional
-            number of the field component to be integrated
-        root : int, optional
-            rank of the leading mpi process (to collect data)
-            If None reduction is done on all processes from topo.
-
-        Returns
-        --------
-        double, integral of a component
-        of the input field over the current subset.
-        """
-        res = self.integrate_field_on_proc(field, topo, component)
-        if root is None:
-            return topo.comm.allreduce(res)
-        else:
-            return topo.comm.reduce(res, root=root)
-
-    def integrate_field_on_proc(self, field, topo, component=0):
-        """Field integration
-
-        Parameters
-        ----------
-        field : :class:`~hysop.fields.continuous_field.Field`
-            a field to be integrated on the box
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-            set mesh/topology used for integration
-        component : int, optional
-            number of the field component to be integrated
-
-        Returns
-        --------
-        double, integral of a component
-        of the input field over the current subset, on the current process
-        (i.e. no mpi reduce over all processes)
-        """
-        assert isinstance(field, Field)
-        assert isinstance(topo, CartesianTopology)
-        discr_f = field.discretize(topo)
-        dvol = npw.prod(topo.mesh.space_step[self.t_dir])
-        result = npw.real_sum(discr_f[component][self.ind[topo][0]])
-        result *= dvol
-        return result
-
-    def integrate_dfield_allc(self, field, root=None):
-        """Field integration
-
-        Parameters
-        ----------
-        field : :class:`~hysop.fields.discrete_field.DiscreteField`
-            a field to be integrated on the box
-        root : int, optional
-            rank of the leading mpi process (to collect data)
-            If None reduction is done on all processes from topo.
-
-        Returns
-        --------
-        a numpy array, with res[i] = integral of component i
-        of the input field over the current subset.
-        """
-        res = npw.zeros(field.nb_components)
-        gres = npw.zeros(field.nb_components)
-        for i in xrange(res.size):
-            res[i] = self.integrate_dfield_on_proc(field, component=i)
-        if root is None:
-            field.topology.comm.Allreduce(res.handle(), gres.handle())
-        else:
-            field.topology.comm.Reduce(res.handle(), gres.handle(), root=root)
-
-        return gres
-
-    def integrate_dfield(self, field, component=0, root=None):
-        """Field integration
-
-        Parameters
-        ----------
-        field : :class:`~hysop.fields.discrete_field.DiscreteField`
-            a field to be integrated on the box
-        component : int, optional
-            number of the field component to be integrated
-        root : int, optional
-            rank of the leading mpi process (to collect data)
-            If None reduction is done on all processes from topo.
-
-        Returns
-        --------
-        double, integral of a component
-        of the input field over the current subset.
-        """
-        res = self.integrate_dfield_on_proc(field, component)
-        if root is None:
-            return field.topology.comm.allreduce(res)
-        else:
-            return field.topology.comm.reduce(res, root=root)
-
-    def integrate_dfield_on_proc(self, field, component=0):
-        """Field integration
-
-        Parameters
-        ----------
-        field : :class:`~hysop.fields.discrete_field.DiscreteField`
-            a field to be integrated on the box
-        component : int, optional
-            number of the field component to be integrated
-
-        Returns
-        --------
-        double, integral of a component
-        of the input field over the current subset, on the current process
-        (i.e. no mpi reduce over all processes)
-        """
-        assert isinstance(field, DiscreteField)
-        topo = field.topology
-        dvol = npw.prod(topo.mesh.space_step[self.t_dir])
-        result = npw.real_sum(field[component][self.ind[topo][0]])
-        result *= dvol
-        return result
-
-
-class Sphere(Subset):
-    """Spherical domain.
-    """
-
-    def __init__(self, origin, radius=1.0, **kwds):
-        """
-        Parameters
-        ----------
-        origin : :class:`~hysop.domain.subsets.Subset`
-
-        origin : list or array
-            position of the center
-        radius : double, optional
-        kwds : base class parameters
-
-        """
-        def dist(*args):
-            size = len(args)
-            return npw.asarray(np.sqrt(sum([(args[d] - self.origin[d]) ** 2
-                                            for d in xrange(size)]))
-                               - self.radius)
-
-        super(Sphere, self).__init__(chi=dist, **kwds)
-        # Radius of the sphere
-        self.radius = radius
-        # Center position
-        self.origin = npw.asrealarray(origin).copy()
-
-    def __str__(self):
-        s = self.__class__.__name__ + ' of radius ' + str(self.radius)
-        s += ' and center position ' + str(self.origin)
-        return s
-
-
-class HemiSphere(Sphere):
-    """HemiSpherical domain.
-    Area defined by the intersection of a sphere and a box.
-    The box is defined with :
-    - cutdir, normal direction to a plan
-    - cutpos, position of this plan along the 'cutdir" axis
-    - all points of the domain where x < xs.
-    """
-    def __init__(self, cutpos=None, cutdir=0, **kwds):
-        """
-        Parameters
-        ----------
-        cutpos : list or array of coordinates
-            position of the cutting plane
-        cutdir : real, optional
-            direction of the normal to the cutting plane.
-            Default = x-direction (0).
-        """
-        super(HemiSphere, self).__init__(**kwds)
-        # direction of normal to the cutting plane
-        self.cutdir = cutdir
-        if cutpos is None:
-            cutpos = self.origin[self.cutdir]
-
-        def left_box(x):
-            return x - cutpos
-        self.LeftBox = left_box
-
-    def chi(self, topo, *args):
-        """Indicator function of the subset
-
-        Returns
-        -------
-        array of bool
-
-        """
-        tol = la.norm(topo.mesh.space_step) * self._TOLCOEF_
-        return np.logical_and(
-            self._is_inside(*args) <= tol,
-            self.LeftBox(args[self.cutdir]) <=
-            (topo.mesh.space_step[self.cutdir] * 0.5))
-
-
-class Cylinder(Subset):
-    """Cylinder-like domain.
-    """
-
-    def __init__(self, origin, radius=1.0, axis=1, **kwds):
-        """
-        Parameters
-        ----------
-        origin : list or array
-            coordinates of the center
-        radius : double, optional
-             default = 1.
-        axis : int, optional
-           direction of the main axis of the cylinder, default=1 (y)
-
-        """
-
-        def dist(*args):
-            size = len(self._dirs)
-            return npw.asarray(np.sqrt(sum([(args[self._dirs[d]] -
-                                             self.origin[self._dirs[d]]) ** 2
-                                            for d in xrange(size)]))
-                               - self.radius)
-
-        super(Cylinder, self).__init__(chi=dist, **kwds)
-        # Radius of the cylinder
-        self.radius = radius
-        # Main axis position
-        self.origin = npw.asrealarray(origin).copy()
-        # direction of the main axis of the cylinder
-        self.axis = axis
-        dim = self._parent.dimension
-        dirs = np.arange(dim)
-        self._dirs = np.where(dirs != self.axis)[0]
-
-    def chi(self, topo, *args):
-        """Indicator function of the subset
-
-        Returns
-        -------
-        array of bool
-
-        """
-        tol = la.norm(topo.mesh.space_step) * self._TOLCOEF_
-        return np.logical_and(self._is_inside(*args) <= tol,
-                              args[self.axis] ==
-                              args[self.axis])
-
-    def __str__(self):
-        s = self.__class__.__name__ + ' of radius ' + str(self.radius)
-        s += ' and center position ' + str(self.origin)
-        return s
-
-
-class HemiCylinder(Cylinder):
-    """Half cylinder domain.
-    """
-    def __init__(self, cutpos=None, cutdir=0, **kwds):
-        """A cylinder cut by a plane (normal to one axis dir).
-
-        Parameters
-        ----------
-        cutpos : list or array of coordinates
-            position of the cutting plane
-        cutdir : real, optional
-            direction of the normal to the cutting plane.
-            Default = x-direction (0).
-
-        """
-        super(HemiCylinder, self).__init__(**kwds)
-        # direction of normal to the cutting plane
-        self.cutdir = cutdir
-        if cutpos is None:
-            cutpos = self.origin[self.cutdir]
-
-        def left_box(x):
-            return x - cutpos
-        self.LeftBox = left_box
-
-    def chi(self, topo, *args):
-        """Indicator function of the subset
-
-        Returns
-        -------
-        array of bool
-
-        """
-        tol = la.norm(topo.mesh.space_step) * self._TOLCOEF_
-        return (np.logical_and(
-            np.logical_and(self._is_inside(*args) <= tol,
-                           self.LeftBox(args[self.cutdir])
-                           <= topo.mesh.space_step[self.cutdir]),
-            args[self.axis] == args[self.axis]))
-
-
-class SubBox(Subset):
-    """
-    A rectangle (in 2 or 3D space), defined by the coordinates of
-    its lowest point, its lenghts and its normal.
-    """
-    def __init__(self, origin, length, normal=1, **kwds):
-        """
-        Parameters
-        ----------
-        origin : list or array of double
-            position of the lowest point of the box
-        length : list or array of double
-            lengthes of the sides of the box
-        normal : int = 1 or -1, optional
-            direction of the outward normal. Only makes
-            sense when the 'box' is a plane.
-        **kwds : extra args for parent class
-
-        """
-        super(SubBox, self).__init__(**kwds)
-        # Dictionnary of hysop.domain.mesh.Submesh, keys=topo, values=mesh
-        self.mesh = {}
-        # coordinates of the lowest point of this subset
-        self.origin = npw.asrealarray(origin).copy()
-        # length of this subset
-        self.length = npw.asrealarray(length).copy()
-        # coordinates of the 'highest' point of this subset
-        self.end = self.origin + self.length
-        # coordinate axes belonging to the subset
-        self.t_dir = np.where(self.length != 0)[0]
-        # coordinate axe normal to the subset
-        self.n_dir = np.where(self.length == 0)[0]
-        # direction of the outward unit normal (+ or - 1)
-        # Useful when the surface belong to a control box.
-        self.normal = normal
-        msg = 'Subset error, the origin is outside of the domain.'
-        assert ((self.origin - self._parent.origin) >= 0).all(), msg
-        msg = 'Subset error, the subset is too large for the domain.'
-        assert ((self._parent.end - self.end) >= 0).all(), msg
-        # dict of coordinates of the lengthes of the subset (values)
-        # for a given topo (keys). If the origin/length does not
-        # fit exactly with the mesh, there may be a small shift of
-        # these values.
-        self.real_length = {}
-        # dict of coordinates of the origin of the subset (values)
-        # for a given topo (keys). If the origin/length does not
-        # fit exactly with the mesh, there may be a small shift of
-        # these values.
-        self.real_orig = {}
-
-    def discretize(self, topo):
-        """
-        Compute a local submesh on the subset, for a given topology
-
-        :param topo: :class:`~hysop.topology.topology.CartesianTopology`
-        """
-        assert isinstance(topo, CartesianTopology)
-        if topo in self.mesh:
-            return self.ind[topo]
-
-        # Find the indices of starting/ending points in the global_end
-        # grid of the mesh of topo.
-        gstart = topo.mesh.global_indices(self.origin)
-        gend = topo.mesh.global_indices(self.origin + self.length)
-        # create a submesh
-        self.mesh[topo]        = SubMesh(topo.mesh, gstart, gend)
-        self.real_length[topo] = self.mesh[topo].global_length
-        self.real_orig[topo]   = self.mesh[topo].global_origin
-        self.ind[topo]         = [self.mesh[topo].compute_index]
-        self._reduce_topology(topo)
-        return self.ind[topo]
-
-    def chi(self, topo, *args):
-        """
-        indicator function for points inside this submesh.
-        This is only useful when one require the computation
-        of the intersection of a regular subset and a sphere-like
-        subset.
-        See intersection and subtract methods in Subset class.
-
-        param : tuple of coordinates (like topo.mesh.coords)
-
-        returns : an array of boolean (True if inside)
-        """
-        msg = 'You must discretize the SubBox before any call to chi function.'
-        assert topo in self.mesh, msg
-        return self.mesh[topo].chi(*args)
-
-    def _reduce_topology(self, topo):
-        """Find the reduced mpi communicator that handles
-        all points of this subset.
-        """
-        self.on_proc[topo] = self.mesh[topo].on_proc
-        plist = np.asarray(topo.comm.allgather(self.on_proc[topo]),
-                           dtype=np.bool)
-        gtopo = topo.comm.Get_group()
-        rks = np.where(plist)[0]
-        subgroup = gtopo.Incl(rks)
-        self.subcomm[topo] = topo.comm.Create(subgroup)
-        self.max_subcoords[topo] = None
-        if self.on_proc[topo]:
-            self.max_subcoords[topo] = topo.proc_coords.copy()
-            print topo.proc_coords.__class__
-            print topo.proc_coords.__array_interface__
-            self.subcomm[topo].Allreduce(topo.proc_coords.handle(),
-                                         self.max_subcoords[topo].handle(),
-                                         op=MPI.MAX)
-
-    def integrate_field_on_proc(self, field, topo, component=0):
-        """Field integration
-
-        Parameters
-        ----------
-        field : :class:`~hysop.field.continuous.Field`
-            a field to be integrated on the box
-        topo : :class:`~hysop.topology.topology.CartesianTopology`
-            set mesh/topology used for integration
-        component : int, optional
-            number of the field component to be integrated
-
-        Returns
-        --------
-        integral of a component of the input field over the current subset,
-        on the current process
-        (i.e. no mpi reduce over all processes) for the discretization
-        given by topo..
-        """
-        assert isinstance(field, Field)
-        assert isinstance(topo, CartesianTopology)
-        df = field.discretize(topo)
-        dvol = npw.prod(topo.mesh.space_step[self.t_dir])
-        result = npw.real_sum(df[component][self.mesh[topo].ind4integ])
-        result *= dvol
-        return result
-
-    def integrate_func(self, func, topo, nbc, root=None):
-        """Function integration
-
-        Parameters
-        ----------
-        func: python function of space coordinates
-        topo: :class:`~hysop.topology.topology.CartesianTopology`
-            set mesh/topology used for integration
-        nbc : int
-            number of components of the return value from func
-
-        Returns
-        -------
-        integral of a component of the input field
-        over the current subset, for the discretization given by
-        topo
-        """
-        res = npw.zeros(nbc)
-        gres = npw.zeros(nbc)
-        for i in xrange(res.size):
-            res[i] = self.integrate_func_on_proc(func, topo)
-        if root is None:
-            topo.comm.Allreduce(res.handle(), gres.handle())
-        else:
-            topo.comm.Reduce(res.handle(), gres.handle(), root=root)
-        return gres
-
-    def integrate_func_on_proc(self, func, topo):
-        """Function local integration
-
-        Parameters
-        ----------
-        func: python function of space coordinates
-        topo: :class:`~hysop.topology.topology.CartesianTopology`
-            set mesh/topology used for integration
-
-        Returns
-        --------
-        integral of the function on the subset on the current process
-        (i.e. no mpi reduce over all processes)
-        """
-        assert hasattr(func, '__call__')
-        assert isinstance(topo, CartesianTopology)
-        vfunc = np.vectorize(func)
-        if self.mesh[topo].on_proc:
-            result = npw.real_sum(vfunc(*self.mesh[topo].coords4int))
-        else:
-            result = 0.
-        dvol = npw.prod(topo.mesh.space_step)
-        result *= dvol
-        return result
-
-    def integrate_dfield_on_proc(self, field, component=0):
-        """Discrete field local integration
-
-        Parameters
-        ----------
-        field : :class:`~hysop.field.discrete.DiscreteField`
-            a field to be integrated on the box
-        component : int, optional
-            number of the field component to be integrated
-        integrate the field on the subset on the current process
-        (i.e. no mpi reduce over all processes) for the discretization
-        given by topo.
-
-        Returns
-        --------
-        integral of the field on the subset on the current process
-        (i.e. no mpi reduce over all processes)
-        """
-        assert isinstance(field, DiscreteField)
-        topo = field.topology
-        dvol = npw.prod(topo.mesh.space_step[self.t_dir])
-        result = npw.real_sum(field[component][self.mesh[topo].ind4integ])
-        result *= dvol
-        return result
diff --git a/hysop/numerics/fft/fft.py b/hysop/numerics/fft/fft.py
index 5a2ff8b079492da2731e50669e888b66e63e6163..e6ef39eb1e6c97438ca3640fd9aa8c345e9acfb7 100644
--- a/hysop/numerics/fft/fft.py
+++ b/hysop/numerics/fft/fft.py
@@ -1,22 +1,44 @@
 """
 Base interface for fast Fourier Transforms.
-:class:`~hysop.numerics.fft.FFTPlanI`
+
 :class:`~hysop.numerics.fft.FFTI`
+:class:`~hysop.numerics.fft.FFTPlanI`
+:class:`~hysop.numerics.fft.FFTQueueI`
+
+:class:`~hysop.numerics.fft.HysopFFTWarning`
+:class:`~hysop.numerics.fft.HysopFFTDataLayoutError`
+
+Methods defined here:
+  simd_alignment, is_byte_aligned, mk_shape, mk_view
 """
 
 from abc import ABCMeta, abstractmethod
 import warnings
+import functools
 import numpy as np
+from pyfftw import simd_alignment, is_byte_aligned as is_simd_byte_aligned
 
-from hysop.tools.types import first_not_None
+from hysop import vprint, __VERBOSE__
+from hysop.constants import Backend, TransformType
+from hysop.tools.types import first_not_None, check_instance
 from hysop.tools.numerics import float_to_complex_dtype, complex_to_float_dtype
 from hysop.tools.units import bytes2str
-from hysop.backend.host.host_array_backend import HostArrayBackend
-from hysop.backend.host.host_array import HostArray
-
 from hysop.tools.warning import HysopWarning
-class HysopFFTWarning(HysopWarning):
-    pass
+from hysop.core.arrays.array import Array
+from hysop.core.arrays.array_backend import ArrayBackend
+
+
+def is_byte_aligned(array):
+    from hysop.backend.host.host_array import HostArray
+    from hysop.backend.device.opencl.opencl_array import OpenClArray, clArray
+    if isinstance(array, (HostArray, np.ndarray)):
+        return is_simd_byte_aligned(array)
+    elif isinstance(array, (OpenClArray, clArray.Array)):
+        return (array.offset == 0)
+    else:
+        msg='Unknown array type {}.'.format(type(array))
+        raise TypeError(msg)
+
 
 def mk_shape(base_shape, axis, N):
     """
@@ -27,6 +49,7 @@ def mk_shape(base_shape, axis, N):
     shape[axis] = N
     return tuple(shape)
 
+
 def mk_view(ndim, axis, *args, **kwds):
     """
     Utility function to create a view on a n-dimensional array.
@@ -45,16 +68,49 @@ def mk_view(ndim, axis, *args, **kwds):
     return tuple(view)
 
 
+class HysopFFTWarning(HysopWarning):
+    """
+    Specific tag to issue FFT related warnings.
+    """
+    pass
+
+
+class HysopFFTDataLayoutError(ValueError):
+    """
+    Specific error to raise for incompatible strides.
+    """
+    pass
+
+
+class FFTQueueI(object):
+    """Command queue like objects to define n-dimensional transforms."""
+    @abstractmethod
+    def execute(self):
+        """Execute all planned plans."""
+        pass
+    
+    @abstractmethod
+    def __iadd__(self, *plans):
+        """Add a plan to the queue."""
+        pass
+
+    def __call__(self):
+        """Alias for execute."""
+        return self.execute()
+
+
 class FFTPlanI(object):
     """
     Common inteface for FFT plans.
     Basically just a functor that holds relevant data
-    to execute a preconfigurarted FFT-like tranform.
+    to execute a preconfigurated FFT-like tranform.
     """
     __metaclass__ = ABCMeta
 
-    def __init__(self):
+    def __init__(self, verbose=__VERBOSE__):
+        self.verbose = verbose
         self._setup = False
+        self._allocated = False
 
     @abstractmethod
     def input_array(self):
@@ -70,7 +126,7 @@ class FFTPlanI(object):
         """
         pass
     
-    def setup(self):
+    def setup(self, queue=None):
         """
         Method that has to be called before any call to execute.
         """
@@ -79,12 +135,29 @@ class FFTPlanI(object):
             raise RuntimeError(msg)
         self._setup = True
         return self
+    
+    @property
+    def required_buffer_size(self):
+        """
+        Return the required temporary buffer size in bytes to 
+        compute the transform.
+        """
+        assert self._setup
+        return 0
+
+    def allocate(self, buf=None):
+        """Provide or allocate required temporary buffer."""
+        assert self._setup
+        assert not self._allocated
+        self._allocated = True
+    
 
     @abstractmethod
     def execute(self):
         """
         Execute the FFT plan on current input and output array.
         """
+        pass
 
     def __call__(self, a=None, out=None, **kwds):
         """
@@ -105,14 +178,10 @@ class FFTI(object):
     Standard FFTs: complex to complex (C2C)
         fft()	Compute the 1-dimensional discrete Fourier Transform.
         ifft()	Compute the 1-dimensional inverse discrete Fourier Transform.
-        fftn()	Compute the N-dimensional discrete Fourier Transform.
-        ifftn()	Compute the N-dimensional inverse discrete Fourier Transform.
 
     Real data FFTS: real to complex (R2C) and complex to real (C2R)
         rfft()	 Compute the 1-dimensional discrete Fourier Transform for real input.
         irfft()  Compute the inverse of the 1-dimensional FFT of real input.
-        rfftn()	 Compute the N-dimensional discrete Fourier Transform for real input.
-        irfftn() Compute the inverse of the N-dimensional FFT of real input.
 
     Real FFTS: real to real (R2R)
         dct()    Compute one of the discrete cosine transforms of a real input.
@@ -128,11 +197,11 @@ class FFTI(object):
     About floating point precision:
         By default, both simple and double precision are supported. 
         numpy only supports double precision (simple precision is supported by casting).
-        FFTW supports long double precision.
+        FFTW also supports long double precision.
     
     Normalization:
-        The default normalization has the direct transforms unscaled and the inverse transforms are 
-        scaled by 1/N where N is the logical size of the transform. 
+        The default normalization has the direct transforms unscaled and the inverse transform
+        is scaled by 1/N where N is the logical size of the transform. 
         N should not to be confused with the physical size of the input arrays n:
 
         FFT, RFFT:               N = n
@@ -144,35 +213,178 @@ class FFTI(object):
         Orthogonal normalization is not supported by default, however the user
         may specify its custom normalization for each transform via the 'scaling'
         keyword parameter.
+
+    Inverse transforms (up to scaling):
+        Just add i in front of the method name to get the inverse transform with good scaling.
+        For a given transform T, iT(T(X)) should always yield X within machine accuracy.
+
+        Underlying inverse transform mapping is:
+            FFT:     IFFT
+            RFFT:    IRFFT
+
+            DCT-I:   DCT-I
+            DCT-II:  DCT-III
+            DCT-III: DCT-II
+            DCT-IV:  DCT-IV
+
+            DST-I:   DST-I
+            DST-II:  DST-III
+            DST-III: DST-II
+            DST-IV:  DST-IV
+
+    Other methods that this interface defines:
+        *Create queue
+        *Transpose
+        *Copy
+        *Zero fill
+    Those methods will be used by the n-dimensional planner.
     """
     __metaclass__ = ABCMeta
-
-
-    def __init__(self, backend=None, warn_on_allocation=True):
+        
+    __transform2fn = {
+        TransformType.FFT:      ('fft',   {}),
+        TransformType.IFFT:     ('ifft',  {}),
+        TransformType.RFFT:     ('rfft',  {}),
+        TransformType.IRFFT:    ('irfft', {}),
+        TransformType.DCT_I:    ('dct',   {'type': 1}),
+        TransformType.DCT_II:   ('dct',   {'type': 2}),
+        TransformType.DCT_III:  ('dct',   {'type': 3}),
+        TransformType.DCT_IV:   ('dct',   {'type': 4}),
+        TransformType.IDCT_I:   ('idct',  {'type': 1}),
+        TransformType.IDCT_II:  ('idct',  {'type': 2}),
+        TransformType.IDCT_III: ('idct',  {'type': 3}),
+        TransformType.IDCT_IV:  ('idct',  {'type': 4}),
+        TransformType.DST_I:    ('dst',   {'type': 1}),
+        TransformType.DST_II:   ('dst',   {'type': 2}),
+        TransformType.DST_III:  ('dst',   {'type': 3}),
+        TransformType.DST_IV:   ('dst',   {'type': 4}),
+        TransformType.IDST_I:   ('idst',  {'type': 1}),
+        TransformType.IDST_II:  ('idst',  {'type': 2}),
+        TransformType.IDST_III: ('idst',  {'type': 3}),
+        TransformType.IDST_IV:  ('idst',  {'type': 4}),
+    }
+    
+    @classmethod
+    def default_interface_from_backend(cls, backend, 
+            enable_opencl_host_buffer_mapping, **kwds):
+        check_instance(backend, ArrayBackend)
+        if (backend.kind is Backend.HOST):
+            from hysop.numerics.fft.host_fft import HostFFTI
+            assert not enable_opencl_host_buffer_mapping
+            return HostFFTI.default_interface(**kwds)
+        elif (backend.kind is Backend.OPENCL):
+            if enable_opencl_host_buffer_mapping:
+                from hysop.numerics.fft.host_fft import HostFFTI
+                return HostFFTI.default_interface(backend=backend.host_array_backend, **kwds)
+            else:
+                from hysop.numerics.fft.opencl_fft import OpenClFFTI
+                return OpenClFFTI.default_interface(cl_env=backend.cl_env, **kwds)
+        else:
+            msg='Unknown backend kind {}.'.format(backend.kind)
+
+    def check_backend(self, backend,
+            enable_opencl_host_buffer_mapping):
+        check_instance(backend, ArrayBackend)
+        if enable_opencl_host_buffer_mapping:
+            if (self.backend is not backend.host_array_backend):
+                msg='Host array backend mismatch {} vs {}.'
+                msg=msg.format(self.backend, backend)
+                raise RuntimeError(msg)
+        else:
+            if (self.backend is not backend):
+                msg='Backend mismatch {} vs {}.'
+                msg=msg.format(self.backend, backend)
+                raise RuntimeError(msg)
+    
+    def get_transform(self, transform):
+        check_instance(transform, TransformType)
+        if (transform not in self.__transform2fn):
+            msg='Unknown transform type {}.'.format(transform)
+            raise NotImplementedError(transform)
+        (fname, fkwds) = self.__transform2fn[transform]
+        fn = getattr(self, fname)
+        if fkwds:
+            fn = functools.partial(fn, **fkwds)
+        return fn
+
+    def __init__(self, backend, 
+            warn_on_allocation=True,
+            error_on_allocation=False):
         """Initializes the interface and default supported real and complex types."""
+        from hysop.core.arrays.array_backend import ArrayBackend
+        check_instance(backend, ArrayBackend)
+        check_instance(warn_on_allocation, bool)
+        check_instance(error_on_allocation, bool)
+
         self.supported_ftypes = (np.float32, np.float64)
         self.supported_ctypes = (np.complex64, np.complex128)
         self.supported_cosine_transforms = (1,2,3)
         self.supported_sine_transforms   = (1,2,3)
 
-        if (backend is None):
-            backend = HostArrayBackend.get_or_create(allocator=None)
         self.backend = backend
-        self.warn_on_allocation = warn_on_allocation
+        self.warn_on_allocation  = warn_on_allocation
+        self.error_on_allocation = error_on_allocation
    
     def allocate_output(self, out, shape, dtype):
         """Alocate output if required and check shape and dtype."""
         if (out is None):
-            if self.warn_on_allocation:
+            if self.warn_on_allocation or self.error_on_allocation:
                 nbytes = np.prod(shape, dtype=np.int64)*dtype.itemsize
                 msg='FftwFFT: allocating aligned output array of size {}.'
                 msg=msg.format(bytes2str(nbytes))
-                warnings.warn(msg, HysopFFTWarning)
+                if self.error_on_allocation:
+                    raise RuntimeError(msg)
+                else:
+                    warnings.warn(msg, HysopFFTWarning)
             out = self.backend.empty(shape=shape, dtype=dtype)
         else:
             assert out.dtype == dtype
             assert out.shape == shape
         return out
+    
+    @classmethod
+    def default_interface(cls, **kwds):
+        """Get the default FFT interface."""
+        msg='{}.default_interface() has not been implemented yet !'
+        msg=msg.format(cls.__name__)
+        raise NotImplementedError(msg)
+
+    def allocate_plans(self, op, plans, tmp_buffer=None):
+        """Allocate and share a buffer on given backend to a group of plans."""
+        backend = self.backend
+        tmp_size = max(plan.required_buffer_size for plan in plans)
+
+        if (tmp_size>0):
+            msg='Operator {}: Allocating an additional {} temporary buffer for FFT backend {}.'
+            msg=msg.format(op.pretty_name, bytes2str(tmp_size), self.__class__.__name__)
+            if (tmp_buffer is not None):
+                assert tmp_buffer.nbytes >= tmp_size
+            else:
+                if self.error_on_allocation:
+                    raise RuntimeError(msg)
+                elif self.warn_on_allocation:
+                    warnings.warn(msg, HysopGpyFftWarning)
+                else:
+                    vprint(msg)
+                tmp_buffer = backend.empty(shape=(tmp_size), dtype=np.uint8)
+            for plan in plans:
+                if (plan.required_buffer_size > tmp_buffer.nbytes):
+                    msg='\nFATAL ERROR: Failed to allocate temporary buffer for clFFT.'
+                    msg+='\n => clFFT expected {} bytes but only {} bytes have been '
+                    msg+='allocated.\n'
+                    msg=msg.format(plan.required_buffer_size, tmp_buffer.nbytes)
+                    raise RuntimeError(msg)
+                elif (plan.required_buffer_size>0):
+                    buf = tmp_buffer[:plan.required_buffer_size]
+                    plan.allocate(buf=buf)
+                else:
+                    plan.allocate()
+        else:
+            for plan in plans:
+                assert plan.required_buffer_size == 0
+                plan.allocate()
+            tmp_buffer = None
+        return tmp_buffer
 
     @abstractmethod
     def fft(self, a, out, axis=-1, **kwds):
@@ -231,13 +443,13 @@ class FFTI(object):
         
         Returns
         -------
-        (shape, dtype) of the output array determined from the input array.
+        (shape, dtype, logical_size) of the output array determined from the input array.
         """
         assert a.dtype in self.supported_ctypes, a.dtype
         if (out is not None):
             assert a.dtype == out.dtype
             assert np.array_equal(a.shape, out.shape)
-        return (a.shape, a.dtype)
+        return (a.shape, a.dtype, a.shape[axis])
 
 
     @abstractmethod
@@ -317,7 +529,8 @@ class FFTI(object):
         
         Returns
         -------
-        (shape, dtype) of the output array determined from the input array, out and n.
+        (shape, dtype, logical_size) of the output array determined from the input array, 
+        out and n.
         """
         assert a.dtype   in self.supported_ctypes
         cshape = a.shape
@@ -344,11 +557,14 @@ class FFTI(object):
 
         if (n is None) or (n%2==0):
             rshape = rshape_even
+            n = rshape[axis]
         else:
             rshape = rshape_odd
         
         rshape = tuple(rshape)
-        return (rshape, rtype)
+        logical_size = n
+        assert rshape[axis] == logical_size
+        return (rshape, rtype, logical_size)
     
     @abstractmethod
     def dct(self, a, out=None, type=2, axis=-1, **kwds):
@@ -379,6 +595,9 @@ class FFTI(object):
     def idct(self, a, out=None, type=2, axis=-1, **kwds):
         """
         Compute the one-dimensional Inverse Cosine Transform of specified type.
+        
+        Default scaling is 1/(2*N)   for IDCT type (2,3,4) and 
+                           1/(2*N-2) for IDCT type 1.
 
         Parameters
         ----------
@@ -391,7 +610,8 @@ class FFTI(object):
             Defaults to last axis.
         Returns
         -------
-        (shape, dtype) of the output array determined from the input array.
+        (shape, dtype, inverse_type, logical_size) of the output array determined from the input 
+        array.
         """
         itype = [1,3,2,4][type-1]
         n = a.shape[axis]
@@ -409,9 +629,6 @@ class FFTI(object):
         """
         Compute the one-dimensional Sine Transform of specified type.
 
-        Default scaling is 1/(2*N)   for IDCT type (2,3,4) and 
-                           1/(2*N-2) for IDCT type 1.
-
         Parameters
         ----------
         a: array_like
@@ -451,7 +668,8 @@ class FFTI(object):
             Defaults to last axis.
         Returns
         -------
-        (shape, dtype) of the output array determined from the input array.
+        (shape, dtype, inverse_type, logical_size) of the output array determined from the input 
+        array.
         """
         itype = [1,3,2,4][type-1]
         n = a.shape[axis]
@@ -464,3 +682,28 @@ class FFTI(object):
             assert np.array_equal(a.shape, out.shape)
         return (a.shape, a.dtype, itype, logical_size)
 
+    @abstractmethod
+    def new_queue(self, tg, name):
+        """Return a FFTQueue object valid with this backend."""
+        pass
+
+    @abstractmethod
+    def plan_copy(self, tg, src, dst):
+        """Plan a copy from src to dst."""
+        pass
+    
+    @abstractmethod
+    def plan_accumulate(self, tg, src, dst):
+        """Plan an accumulation from src into dst."""
+        pass
+
+    @abstractmethod
+    def plan_transpose(self, tg, src, dst, axes):
+        """Plan a transpose from src to dst using given axes."""
+        pass
+
+    @abstractmethod
+    def plan_fill_zeros(self, tg, a, slices):
+        """Plan to fill every input slices of input array a with zeroes."""
+        pass
+
diff --git a/hysop/numerics/fft/fftw_fft.py b/hysop/numerics/fft/fftw_fft.py
index 4086472b4d333513ca499dc241d2325c51f1fdd9..6af22b3144fae997a757e872a751ed4e8c754302 100644
--- a/hysop/numerics/fft/fftw_fft.py
+++ b/hysop/numerics/fft/fftw_fft.py
@@ -1,5 +1,5 @@
 """
-FFT iterface for fast Fourier Transforms using FFTW backend.
+FFT iterface for fast Fourier Transforms using FFTW backend (using pyfftw).
 :class:`~hysop.numerics.FftwFFT`
 :class:`~hysop.numerics.FftwFFTPlan`
 """
@@ -8,20 +8,48 @@ import warnings
 import pyfftw
 import numpy as np
 
+from hysop import __FFTW_NUM_THREADS__, __FFTW_PLANNER_EFFORT__, __FFTW_PLANNER_TIMELIMIT__, __VERBOSE__
+from hysop.tools.io_utils   import IO 
 from hysop.tools.types import first_not_None
 from hysop.tools.misc import prod
-from hysop.numerics.fft.fft import FFTPlanI, FFTI, HostArray, \
-        HysopFFTWarning, bytes2str
+from hysop.tools.string_utils import framed_str
+from hysop.tools.cache import load_data_from_cache, update_cache
+from hysop.numerics.fft.fft import HysopFFTWarning, bytes2str
+from hysop.numerics.fft.host_fft import HostFFTPlanI, HostFFTI, HostArray
 
-class FftwFFTPlan(FFTPlanI):
+class FftwFFTPlan(HostFFTPlanI):
     """
     Build and wraps a FFTW plan.
     Emit warnings when SIMD alignment is not used.
     Emit warnings when changing input and output alignment.
     """
 
+    __FFTW_USE_CACHE__=True
+    
+    @classmethod
+    def cache_file(cls):
+        _cache_dir  = IO.cache_path() + '/numerics'
+        _cache_file = _cache_dir + '/fftw_wisdom.pklz'
+        return _cache_file
+
+    @classmethod
+    def load_wisdom(cls, h):
+        if cls.__FFTW_USE_CACHE__:
+            wisdom = load_data_from_cache(cls.cache_file(), h)
+            if (wisdom is not None):
+                pyfftw.import_wisdom(wisdom)
+                return True
+        return False
+        
+    @classmethod
+    def save_wisdom(cls, h, plan):
+        if cls.__FFTW_USE_CACHE__:
+            wisdom = pyfftw.export_wisdom()
+            update_cache(cls.cache_file(), h, wisdom)
+
     def __init__(self, a, out, scaling=None, **plan_kwds):
-        super(FftwFFTPlan, self).__init__()
+        verbose = plan_kwds.pop('verbose', __VERBOSE__)
+        super(FftwFFTPlan, self).__init__(verbose=verbose)
 
         if isinstance(a, HostArray):
             plan_kwds['input_array'] = a.handle
@@ -32,12 +60,62 @@ class FftwFFTPlan(FFTPlanI):
             plan_kwds['output_array'] = out.handle
         else:
             plan_kwds['output_array'] = out
+        
+        def fmt_arg(name):
+            return plan_kwds[name]
+        def fmt_array(name):
+            arr = fmt_arg(name)
+            return 'shape={:<16} strides={:<16} dtype={:<16}'.format(
+                    str(arr.shape)+',', 
+                    str(arr.strides)+',',
+                    arr.dtype)
 
-        plan = pyfftw.FFTW(**plan_kwds)
+        title=' Planning {} '.format(self.__class__.__name__) 
+        msg = \
+        '''    in_array:           {}
+    out_array:          {}
+    axes:               {}
+    direction:          {}
+    threads:            {}
+    flags:              {}
+    planning timelimit: {}'''.format(
+            fmt_array('input_array'),
+            fmt_array('output_array'),
+            fmt_arg('axes'),
+            fmt_arg('direction'),
+            fmt_arg('threads'),
+            ' | '.join(fmt_arg('flags')),
+            fmt_arg('planning_timelimit'))
+        if self.verbose:
+            print
+            print framed_str(title, msg, c='*')
+        
+        def hash_arg(name):
+            return hash(plan_kwds[name])
+        def hash_array(name):
+            arr = plan_kwds[name]
+            return hash(arr.shape) ^ hash(arr.strides)
+        #h = hash_array('input_array') ^ hash_array('output_array') ^ hash_arg('axes') ^ hash_arg('direction')
+        h = None
+        
+        plan = None
+        may_have_wisdom = self.load_wisdom(h)
+        if may_have_wisdom:
+            plan_kwds['flags'] += ('FFTW_WISDOM_ONLY',)
+            # try to build plan from wisdom only (can fail if wisdom has only measure knowledge for example)
+            try:
+                plan = pyfftw.FFTW(**plan_kwds)
+            except RuntimeError:
+                pass
+        if (plan is None):
+            plan_kwds['flags'] = tuple(set(plan_kwds['flags']) - set(['FFTW_WISDOM_ONLY']))
+            plan = pyfftw.FFTW(**plan_kwds)
+            self.save_wisdom(h, plan)
         if (not plan.simd_aligned):
             msg='Resulting plan is not SIMD aligned ({} bytes boundary).'
             msg=msg.format(pyfftw.simd_alignment)
             warnings.warn(msg, HysopFFTWarning)
+
         self.plan = plan
         self.scaling = scaling
         self.out = out
@@ -79,20 +157,17 @@ class FftwFFTPlan(FFTPlanI):
         if (self.scaling is not None):
             self.output_array[...] *= self.scaling
     
-    def __call__(self, a=None, out=None):
+    def __call__(self):
         """
         Execute the plan on possibly different input and output arrays.
         Input array updates with arrays that are not aligned on original byte boundary 
         will result in a copy being made.
         Return output array for convenience.
         """
-        (a, out) = self.check_new_inputs(a, out)
-        self.plan(input_array=a, output_array=out, normalize_idft=True)
-        if (self.scaling is not None):
-            self.output_array[...] *= self.scaling
+        self.execute()
 
 
-class FftwFFT(FFTI):
+class FftwFFT(HostFFTI):
     """
     Interface to compute local to process FFT-like transforms using the FFTW backend.
 
@@ -105,16 +180,21 @@ class FftwFFT(FFTI):
     Planning destroys initial arrays content.
     """
 
-    def __init__(self, threads=1, 
-                       planner_effort='FFTW_MEASURE', 
+    def __init__(self, threads=None, 
+                       planner_effort=None,
                        planning_timelimit=None,
                        destroy_input=False,
-                       warn_on_allocation=True,
                        warn_on_misalignment=True,
-                       backend=None,
+                       warn_on_allocation=True,
+                       error_on_allocation=False,
+                       backend=None, allocator=None,
                        **kwds):
-        super(FftwFFT, self).__init__(backend=backend, 
-                warn_on_allocation=warn_on_allocation, **kwds)
+        threads            = first_not_None(threads,            __FFTW_NUM_THREADS__)
+        planner_effort     = first_not_None(planner_effort,     __FFTW_PLANNER_EFFORT__)
+        planning_timelimit = first_not_None(planning_timelimit, __FFTW_PLANNER_TIMELIMIT__)
+        super(FftwFFT, self).__init__(backend=backend, allocator=allocator, 
+                warn_on_allocation=warn_on_allocation, error_on_allocation=error_on_allocation,
+                **kwds)
         self.supported_ftypes = (np.float32, np.float64, np.longdouble)
         self.supported_ctypes = (np.complex64, np.complex128, np.clongdouble)
         self.supported_cosine_transforms = (1,2,3,4)
@@ -146,6 +226,7 @@ class FftwFFT(FFTI):
         plan_kwds['direction']          = kwds.pop('direction')
         plan_kwds['axes']               = kwds.pop('axes', (kwds.pop('axis'),))
         plan_kwds['threads']            = kwds.pop('threads', self.threads)
+        plan_kwds['verbose']            = kwds.pop('verbose', __VERBOSE__)
         plan_kwds['planning_timelimit'] = kwds.pop('planning_timelimit', self.planning_timelimit)
 
         flags = ()
@@ -176,7 +257,7 @@ class FftwFFT(FFTI):
 
     def ifft(self, a, out=None, axis=-1, **kwds):
         """Planning destroys initial arrays content."""
-        (shape, dtype) = super(FftwFFT, self).ifft(a=a, out=out, axis=axis, **kwds)
+        (shape, dtype, s) = super(FftwFFT, self).ifft(a=a, out=out, axis=axis, **kwds)
         out = self.allocate_output(out, shape, dtype) 
         if self.warn_on_misalignment:
             self.check_alignment(a, out)
@@ -196,7 +277,7 @@ class FftwFFT(FFTI):
 
     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, 
+        (shape, dtype, s) = 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:
@@ -219,14 +300,14 @@ class FftwFFT(FFTI):
     
     def idct(self, a, out=None, type=2, axis=-1, scaling=None, **kwds):
         """Planning destroys initial arrays content."""
-        (shape, dtype, type, s) = super(FftwFFT, self).idct(a=a, out=out, type=type, axis=axis, 
+        (shape, dtype, itype, s) = super(FftwFFT, self).idct(a=a, out=out, type=type, axis=axis, 
                                         **kwds)
         scaling = first_not_None(scaling, 1.0/s)
         out = self.allocate_output(out, shape, dtype) 
         if self.warn_on_misalignment:
             self.check_alignment(a, out)
         dct_types = ['FFTW_REDFT00', 'FFTW_REDFT10', 'FFTW_REDFT01', 'FFTW_REDFT11']
-        direction = dct_types[int(type)-1]
+        direction = dct_types[int(itype)-1]
         kwds = self.bake_kwds(a=a, out=out, axis=axis, direction=direction, **kwds)
         plan = FftwFFTPlan(scaling=scaling, **kwds)
         return plan
@@ -245,14 +326,14 @@ class FftwFFT(FFTI):
 
     def idst(self, a, out=None, type=2, axis=-1, scaling=None, **kwds):
         """Planning destroys initial arrays content."""
-        (shape, dtype, type, s) = super(FftwFFT, self).idst(a=a, out=out, type=type, 
+        (shape, dtype, itype, s) = super(FftwFFT, self).idst(a=a, out=out, type=type, 
                 axis=axis, **kwds)
         scaling = first_not_None(scaling, 1.0/s)
         out = self.allocate_output(out, shape, dtype) 
         if self.warn_on_misalignment:
             self.check_alignment(a, out)
         dst_types = ['FFTW_RODFT00', 'FFTW_RODFT10', 'FFTW_RODFT01', 'FFTW_RODFT11']
-        direction = dst_types[int(type)-1]
+        direction = dst_types[int(itype)-1]
         kwds = self.bake_kwds(a=a, out=out, axis=axis, direction=direction, **kwds)
         plan = FftwFFTPlan(scaling=scaling, **kwds)
         return plan
diff --git a/hysop/numerics/fft/gpyfft_fft.py b/hysop/numerics/fft/gpyfft_fft.py
index 57a35085c4f90ae7cef8022e0ea28bb7a2ae2d59..341a449fc614670874cc635047dd2c4b79905542 100644
--- a/hysop/numerics/fft/gpyfft_fft.py
+++ b/hysop/numerics/fft/gpyfft_fft.py
@@ -1,57 +1,115 @@
 """
-FFT iterface for fast Fourier Transforms using CLFFT backend.
+FFT iterface for fast Fourier Transforms using CLFFT backend (using gpyfft).
 :class:`~hysop.numerics.GpyFFT`
 :class:`~hysop.numerics.GpyFFTPlan`
 """
 
-import warnings, struct
+import warnings, struct, primefac
 import numpy as np
 from abc import abstractmethod
 
-from gpyfft.fft import gfft, GFFT, FFT as FFTPlan
-from hysop.numerics.fft.fft import FFTPlanI, FFTI, \
+from gpyfft.fft import gfft, GFFT
+from hysop import __KERNEL_DEBUG__, __TRACE_KERNELS__, __VERBOSE__
+from hysop.numerics.fft.fft import HysopFFTDataLayoutError, \
         mk_shape, float_to_complex_dtype, complex_to_float_dtype
+from hysop.numerics.fft.opencl_fft import OpenClFFTPlanI, OpenClFFTI, \
+                                          OpenClArray, OpenClArrayBackend, \
+                                          OpenClFFTQueue
 
 from hysop import vprint
 from hysop.constants import Precision
-from hysop.tools.types import first_not_None
+from hysop.tools.types import first_not_None, check_instance
 from hysop.tools.units import bytes2str
 from hysop.tools.misc import prod
 from hysop.tools.warning import HysopWarning
 from hysop.tools.types import first_not_None
 from hysop.tools.numerics import is_complex, is_fp
+from hysop.tools.string_utils import framed_str
 
 from hysop.backend.device.opencl import cl, clArray
 from hysop.backend.device.codegen.base.variables import dtype_to_ctype
-from hysop.backend.device.opencl.opencl_array_backend import OpenClArrayBackend
+
 
 class HysopGpyFftWarning(HysopWarning):
     pass
 
 
-class GpyFFTPlan(FFTPlanI, FFTPlan):
+class GpyFFTPlan(OpenClFFTPlanI):
     """
     Build a clFFT plan using the gpyfft python interface.
     Emit warnings when transform output has an unaligned buffer offset.
     """
 
-    def __init__(self, cl_env, 
-            in_array, out_array, 
-            axes, direction_forward,
-            scaling=None,
-            scale_by_size=None,
+    DEBUG=False
+
+    def __init__(self, cl_env, in_array, out_array, axes,
+            scaling=None, scale_by_size=None,
+            fake_input=None, fake_output=None,
+            callback_kwds=None,
+            direction_forward=True,
+            hardcode_twiddles=True,
+            warn_on_unaligned_output_offset=True,
             warn_on_allocation=True,
-            warn_on_unaligned_output_offset=True):
+            error_on_allocation=False,
+            **kwds):
         """
         Wrap gpyfft.FFT to allow more versatile callback settings
         and buffer allocations.
+
+        Parameters
+        ----------
+        cl_env: OpenClEnvironment
+            OpenCL environment that will provide a context and a default queue.
+        in_array: cl.Array or OpenClArray
+            Real input array for this transform.
+        out_array: cl.Array or OpenClArray
+            Real output array for this transform.
+        axes: array_like of ints
+            Axis over witch to compute the transform. 
+        scaling: float, optional
+            Force the scaling of the transform.
+            If not given, no scaling is applied (unlike clfft default behaviour).
+            clFFT default scaling for backward transform can be enabled by passing
+            'DEFAULT' to this parameter.
+        scale_by_size: int, optional, defaults to 1
+            Extra scaling by an integer: 1.0/S will be applied during the post callback.
+            This is equivalent to setting scaling to 1.0/S but the two parameters are not
+            mutually exclusive.
+        fake_input: DummyArray, optional
+            Fake array from which are computed transform shape and strides.
+            Only used by R2R transforms.
+        fake_output: DummyArray, optional
+            Fake array from which are computed transform shape and strides.
+            Only used by R2R transforms.
+        direction_forward: bool, optional, defaults to True
+            The direction of the transform. True <=> forward transform.
+        hardcode_twiddles: bool, optional, defaults to True
+            Hardcode twiddles as a __constant static array of complex directly 
+            in the opencl code. Only used by DCT-II, DCT-III, DST-II and DST-III. 
+            If set to False, the twiddles will be computed by the device on the 
+            fly, freeing device __constant memory banks.
+        warn_on_unaligned_output_offset: bool, optional, defaults to True
+            Emit a warning if the planner encounter an output array that has
+            a non zero offset.
+        warn_on_allocation: bool, optional, defaults to True
+            Emit a warning if the planner has to allocate opencl buffers.
+        error_on_allocation: bool, optional, defaults to False
+            Raise a RuntimeError if the planner has to allocate opencl buffers.
         """
-        # we do not want to call FFTPlan.__init__
-        FFTPlanI.__init__(self)
+        super(GpyFFTPlan, self).__init__(**kwds)
+
+        if self.DEBUG:
+            # disable hardcoded twiddles generation to reduce callback sizes
+            hardcode_twiddles=False
+
+        fake_input  = first_not_None(fake_input, in_array)
+        fake_output = first_not_None(fake_output, out_array)
+        callback_kwds = first_not_None(callback_kwds, {})
 
         self.cl_env  = cl_env
-        self.warn_on_allocation = warn_on_allocation
         self.warn_on_unaligned_output_offset = warn_on_unaligned_output_offset
+        self.warn_on_allocation  = warn_on_allocation
+        self.error_on_allocation = error_on_allocation
 
         self.temp_buffer = None
         self._setup      = False
@@ -60,163 +118,549 @@ class GpyFFTPlan(FFTPlanI, FFTPlan):
 
         self.in_array  = in_array
         self.out_array = out_array
+        
+        axes = np.asarray(axes)
+        axes = (axes + in_array.ndim) % in_array.ndim
+        
+        assert in_array.ndim == out_array.ndim
+        assert fake_input.ndim == in_array.ndim
+        assert fake_output.ndim == out_array.ndim
+        assert 0 < axes.size <= in_array.ndim, axes.size
+        
+        scale_by_size = first_not_None(scale_by_size, 1)
 
         self._setup_kwds = {
                 'in_array': in_array,
                 'out_array': out_array,
+                'fake_input': fake_input,
+                'fake_output': fake_output,
                 'axes': axes,
-                'direction_forward': direction_forward,
                 'scaling': scaling,
-                'scale_by_size': scale_by_size
+                'scale_by_size': scale_by_size,
+                'direction_forward': direction_forward,
+                'hardcode_twiddles': hardcode_twiddles,
+                'callback_kwds': callback_kwds
             }
-
-    def setup(self):
-        super(GpyFFTPlan, self).setup()
+    
+    def setup(self, queue=None):
+        super(GpyFFTPlan, self).setup(queue=queue)
         self.setup_plan(**self._setup_kwds)
+        if (queue is not None):
+            check_instance(queue, OpenClFFTQueue)
+            self.bake(queue=queue._queue)
         return self
 
-    def setup_plan(self, in_array, out_array, axes, direction_forward, scaling, scale_by_size):
-        if (scale_by_size is not None):
-            if (scaling is None):
-                scaling = 1.0/scale_by_size
-            else:
-                scaling = scaling / scale_by_size
-
-        axes = np.asarray(axes)
-        assert 0 < axes.size <= 3, axes.size
+    def setup_plan(self, in_array, out_array, 
+            fake_input, fake_output,
+            axes, direction_forward, 
+            scaling, scale_by_size,
+            hardcode_twiddles, callback_kwds):
 
-        # compute strides
-        t_strides_in, t_distance_in, t_batchsize_in, t_shape, axes_transform = \
-                self.calculate_transform_strides(axes, in_array)
+        # compute strides from fake arrays
+        t_strides_in, t_distance_in, t_batchsize_in, t_shape_in, t_axes_in = \
+                self.calculate_transform_strides(axes, fake_input)
 
-        t_strides_out, t_distance_out, t_batchsize_out, t_shape_out, axes_transform_out = \
-                self.calculate_transform_strides(axes, out_array)
+        t_strides_out, t_distance_out, t_batchsize_out, t_shape_out, t_axes_out = \
+                self.calculate_transform_strides(axes, fake_output)
 
-        # check axes
-        msg='Error finding transform axis (consider setting axes argument)'
-        assert np.all(axes_transform == axes_transform_out), msg
+        if not np.array_equal(t_axes_in, t_axes_out):
+            msg='Error finding transform axis (consider setting axes argument)'
+            raise RuntimeError(msg)
+        if not np.array_equal(t_batchsize_in, t_batchsize_out):
+            msg='Batchsize mismatch: {} vs {}.'
+            msg=msg.format(t_batchsize_in, t_batchsize_out)
+            raise RuntimeError(msg)
 
-        # enforce no input and output overlap (unless inplace)
+        # Enforce no input and output overlap (unless inplace)
         t_inplace = False
         if (in_array.base_data == out_array.base_data):
             if (in_array.offset < out_array.offset):
                 assert (in_array.offset + in_array.nbytes) < out_array.offset
             elif (in_array.offset > out_array.offset):
                 assert (out_array.offset + out_array.nbytes) < in_array.offset
-            else:
+            else: # in_array.offset == out_array.offset
                 t_inplace = True
 
-        # check input data type
-        if in_array.dtype in (np.float32, np.complex64):
+        # Check data types
+        single_precision_types = (np.float32, np.complex64)
+        double_precision_types = (np.float64, np.complex128)
+        if in_array.dtype in single_precision_types:
+            valid_precision_types = single_precision_types
             t_precision = gfft.CLFFT_SINGLE
-        elif in_array.dtype in (np.float64, np.complex128):
+            h_precision = Precision.FLOAT
+            fp = 'float'
+        elif in_array.dtype in double_precision_types:
+            valid_precision_types = double_precision_types
             t_precision = gfft.CLFFT_DOUBLE
+            h_precision = Precision.DOUBLE
+            fp = 'double'
         else:
             msg='Unsupported precision {}.'
             msg=msg.format(in_array.dtype)
-            raise RuntimeError(msg)
-
-        if out_array.dtype in (np.float32, np.complex64):
-            t_precision_out = gfft.CLFFT_SINGLE
-        elif out_array.dtype in (np.float64, np.complex128):
-            t_precision_out = gfft.CLFFT_DOUBLE
-        else:
-            msg='Unsupported precision {}.'
-            msg=msg.format(out_array.dtype)
-            raise RuntimeError(msg)
-
-        if (t_precision != t_precision_out):
-            msg='Incompatible input and output precisions: {} vs {}'
-            msg=msg.format(t_precision, t_precision_out)
-            raise RuntimeError(msg)
-
-        if in_array.dtype in (np.float32, np.float64):
+            raise NotImplementedError(msg)
+        
+        for array in (out_array, fake_input, fake_output):
+            if (array.dtype not in valid_precision_types):
+                msg='Incompatible precisions: Got {} but valid precisions are {} '
+                msg+='based on input_array datatype which has been determined to be of kind {}.'
+                msg=msg.format(array.dtype, valid_precision_types, h_precision)
+                raise RuntimeError(msg)
+        
+        # Determine transform layout and expected output shape and dtype
+        float_types   = (np.float32, np.float64)
+        complex_types = (np.complex64, np.complex128)
+        axe0 = t_axes_in[0]
+        if fake_input.dtype in float_types:
             layout_in  = gfft.CLFFT_REAL
             layout_out = gfft.CLFFT_HERMITIAN_INTERLEAVED
-
-            expected_out_shape = list(in_array.shape)
-            expected_out_shape[axes_transform[0]] = \
-                    expected_out_shape[axes_transform[0]]//2 + 1
-            msg='output array shape {} does not match expected shape: {}'
-            msg=msg.format(out_array.shape, expected_out_shape)
-            assert out_array.shape == tuple(expected_out_shape), msg
-        elif in_array.dtype in (np.complex64, np.complex128):
-            if out_array.dtype in (np.complex64, np.complex128):
-                layout_in  = gfft.CLFFT_COMPLEX_INTERLEAVED
-                layout_out = gfft.CLFFT_COMPLEX_INTERLEAVED
-            else:
+            expected_output_shape = mk_shape(fake_input.shape, 
+                    axe0, fake_input.shape[axe0]//2 +1)
+            expected_output_dtype = float_to_complex_dtype(fake_input.dtype)
+            t_shape = t_shape_in
+        elif fake_input.dtype in complex_types:
+            if fake_output.dtype in float_types:
                 layout_in  = gfft.CLFFT_HERMITIAN_INTERLEAVED
                 layout_out = gfft.CLFFT_REAL
+                expected_output_shape = mk_shape(fake_input.shape,
+                        axe0, 2*(fake_input.shape[axe0]-1))
+                expected_output_dtype = complex_to_float_dtype(fake_input.dtype)
                 t_shape = t_shape_out
-
-        if t_inplace and ((layout_in is gfft.CLFFT_REAL) or
-                          (layout_out is gfft.CLFFT_REAL)):
-            assert ((in_array.strides[axes_transform[0]] == in_array.dtype.itemsize) and \
-                    (out_array.strides[axes_transform[0]] == out_array.dtype.itemsize)), \
-                    'inline real transforms need stride 1 for first transform axis'
-
-        plan = GFFT.create_plan(self.context, t_shape)
-        plan.inplace     = t_inplace
-        plan.strides_in  = t_strides_in
-        plan.strides_out = t_strides_out
-        plan.distances   = (t_distance_in, t_distance_out)
-        plan.batch_size  = t_batchsize_in
-        plan.precision   = t_precision
-        plan.layouts     = (layout_in, layout_out)
-        if (scaling is not None):
-            if direction_forward:
-                plan.scale_forward = scaling
+            elif fake_output.dtype in complex_types:
+                layout_in  = gfft.CLFFT_COMPLEX_INTERLEAVED
+                layout_out = gfft.CLFFT_COMPLEX_INTERLEAVED
+                expected_output_shape = fake_input.shape
+                expected_output_dtype = fake_input.dtype
+                t_shape = t_shape_in
+            else:
+                msg='dtype {} is currently not handled.'
+                msg=msg.format(fake_output.dtype)
+                raise NotImplementedError(msg)
+        else:
+            msg='dtype {} is currently not handled.'
+            msg=msg.format(fake_input.dtype)
+            raise NotImplementedError(msg)
+           
+        if (fake_output.dtype != expected_output_dtype):
+            msg='Output array dtype {} does not match expected dtype {}.'
+            msg=msg.format(fake_output.dtype, expected_output_dtype)
+            raise RuntimeError(msg)
+        if not np.array_equal(fake_output.shape, expected_output_shape):
+            msg='Output array shape {} does not match expected shape {}.'
+            msg=msg.format(fake_output.shape, expected_output_shape)
+            if (layout_in  == gfft.CLFFT_HERMITIAN_INTERLEAVED) and \
+               (layout_out == gfft.CLFFT_REAL):
+                expected_output_shape = mk_shape(fake_input.shape,
+                        axe0, 2*(fake_input.shape[axe0]-1) + 1)
+                if not np.array_equal(fake_output.shape, expected_output_shape):
+                    raise RuntimeError(msg)
             else:
-                plan.scale_backward = scaling
+                raise RuntimeError(msg)
 
-        (in_data, out_data) = self.set_callbacks(plan, in_array, out_array, 
-                                                       layout_in, layout_out)
+        if t_inplace and ((layout_in is gfft.CLFFT_REAL) or (layout_out is gfft.CLFFT_REAL)):
+            assert ((in_array.strides[t_axes_in[0]] == in_array.dtype.itemsize) and \
+                    (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]
+        plan.strides_out   = t_strides_out[::-1]
+        plan.distances     = (t_distance_in, t_distance_out)
+        plan.batch_size    = t_batchsize_in
+        plan.precision     = t_precision
+        plan.layouts       = (layout_in, layout_out)
+        if (scaling is 'DEFAULT'):
+            pass
+        elif (scaling is not None):
+            plan.scale_forward  = scale
+            plan.scale_backward = scale
+        else:
+            plan.scale_forward  = 1.0
+            plan.scale_backward = 1.0
 
-        self.plan      = plan
-        self.in_data   = in_data
-        self.out_data  = out_data
+        
+        # last transformed axis real output array size
+        N = out_array.shape[axes[-1]]
+        
+        typegen = self.cl_env.build_typegen(precision=h_precision,
+                                            float_dump_mode='dec', 
+                                            use_short_circuit_ops=False, 
+                                            unroll_loops=False)
+
+        (in_data, out_data) = self.set_callbacks(plan=plan, axes=axes,
+                                                 in_array=in_array, out_array=out_array, 
+                                                 fake_input=fake_input, 
+                                                 fake_output=fake_output,
+                                                 layout_in=layout_in, layout_out=layout_out,
+                                                 N=N, S=scale_by_size, 
+                                                 typegen=typegen, fp=fp,
+                                                 hardcode_twiddles=hardcode_twiddles,
+                                                 **callback_kwds)
+
+        self.plan       = plan
+        self.in_data    = in_data
+        self.out_data   = out_data
         self.is_inplace = t_inplace
         self.direction_forward = direction_forward
+        
+        if self.DEBUG:
+            def estrides(array):
+                s = array.dtype.itemsize
+                return tuple(x//s for x in array.strides)
+            msg=\
+'''
+::CLFFT PLANNER DEBUG::
+Input array:  shape={}, dtype={}, strides={} elements, base_offset={}
+Output array: shape={}, dtype={}, strides={} elements, base_offset={}
+
+Fake input:   shape={}, dtype={}, strides={} elements
+Fake output:  shape={}, dtype={}, strides={} elements
+
+Array configuration:
+    t_distance_in:    {}
+    t_distance_out:   {}
+    t_axes_in:        {}
+    t_axes_out:       {}
+    t_batch_size_in:  {}
+    t_batch_size_out: {}
+    t_shape_in:       {}
+    t_shape_out:      {}
+    t_strides_in:     {}
+    t_strides_out:    {}
+
+Plan configuration:
+    inplace:          {}
+    precision:        {}
+    layouts:          (in={}, out={})
+    shape:            {}
+    strides_in:       {}
+    strides_out:      {}
+    batch_size:       {}
+    distances:        (in={}, out={})
+    scale_forward:    {}
+    scale_backward:   {}
+
+Pre callback source code:
+{}
+
+Post callback source code:
+{}
+'''.format(in_array.shape, in_array.dtype, estrides(in_array), in_array.offset,
+            out_array.shape, out_array.dtype, estrides(out_array), out_array.offset,
+            fake_input.shape, fake_input.dtype, estrides(fake_input), 
+            fake_output.shape, fake_output.dtype, estrides(fake_output), 
+            t_distance_in, t_distance_out, t_axes_in, t_axes_out, t_batchsize_in, t_batchsize_out,
+            t_shape_in, t_shape_out, t_strides_in, t_strides_out,
+            plan.inplace, plan.precision, plan.layouts[0], plan.layouts[1],
+            plan.shape, plan.strides_in, plan.strides_out, plan.batch_size, 
+            plan.distances[0], plan.distances[1], 
+            plan.scale_forward, plan.scale_backward, 
+            self.pre_callback_src, self.post_callback_src)
+            print msg
+        if (scaling is 'DEFAULT'):
+            pass
+        elif (scaling is not None):
+            plan.scale_forward  = scale
+            plan.scale_backward = scale
+        else:
+            plan.scale_forward  = 1.0
+            plan.scale_backward = 1.0
+
+    def set_callbacks(self, plan, axes, N,
+            in_array, out_array, fake_input, fake_output,
+            layout_in, layout_out, **kwds):
+        """Set plan pre and post callbacks and return in and out array data opencl buffers."""
+
+        (in_data, in_fp, oip) = self.compute_input_array_offset(in_array, fake_input, axes)
+        if (layout_in  == gfft.CLFFT_HERMITIAN_INTERLEAVED) and \
+           (layout_out == gfft.CLFFT_REAL):
+            # ********************************************************************************
+            # CLFFT C2R BUGFIX
+            # Force the zero and the Nyquist frequency of the input to be purely real.
+            (pre_src, user_data) = self.pre_offset_callback_C2R(offset_input_pointer=oip, 
+                    in_fp=in_fp, N=N, **kwds)
+            # ********************************************************************************
+        else:
+            (pre_src, user_data) = self.pre_offset_callback(offset_input_pointer=oip, 
+                    in_fp=in_fp, N=N, **kwds)
 
-    def set_callbacks(self, plan, in_array, out_array, layout_in, layout_out, **kwds):
-        in_data = in_array.base_data
-        pre, user_data = self.pre_offset_callback(in_array, layout_in, **kwds)
-
-        out_data = out_array.base_data
-        post, user_data = self.post_offset_callback(out_array, layout_out, **kwds)
+        (out_data, out_fp, oop) = self.compute_output_array_offset(out_array, fake_output, 
+                axes)
+        (post_src, user_data) = self.post_offset_callback(offset_output_pointer=oop, 
+                out_fp=out_fp, N=N, **kwds)
         
-        # **********************************************************************************
+        # ***********************************************************************************
+        # GPYFFT BUGFIX
         # Keep a reference to callback source code to prevent dangling const char* pointers.
         # Do not remove because clfft only get the pointer and gpyfft does not increase the 
-        # refcount of those strings.
-        self.pre_callback_src  = pre
-        self.post_callback_src = post
-        # **********************************************************************************
-
-        plan.set_callback(u'pre_callback',  pre,  'pre',  user_data=user_data)
-        plan.set_callback(u'post_callback', post, 'post', user_data=user_data)
+        # refcount of those strings, resulting in random code injection into the fft kernels.
+        self.pre_callback_src  = pre_src
+        self.post_callback_src = post_src
+        # ***********************************************************************************
+        
+        if (pre_src is not None):
+            plan.set_callback('pre_callback',  pre_src,  'pre',  user_data=user_data)
+        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):
+        """Redefine gpyfft.FFT.calculate_transform_strides"""
+        shape   = np.asarray(array.shape, dtype=np.uint32)
+        strides = np.asarray(array.strides, dtype=np.uint32)
+        dtype   = array.dtype
+        
+        # array dimension and transform dimension
+        ndim = len(shape)
+        tdim = len(taxes)
+        assert tdim <= ndim
+
+        # transform axes and batch axes
+        taxes[taxes<0] += ndim
+        baxes = np.asarray(tuple(a for a in range(ndim) if (a not in taxes)), dtype=np.uint32)
+
+        # sort untransformed axes by strides.
+        baxes = baxes[np.argsort(strides[baxes][::-1])][::-1]
+        
+        # compute a list of collapsable axes:  [ [x,y], [z] ]
+        cal = []                 # collaspsable axes list
+        cac = baxes[:1].tolist() # collaspsable axes candidates
+        for a in baxes[1:]:
+            if strides[a] == (strides[cac[-1]] * shape[cac[-1]]):
+                cac.append(a)
+            else:
+                cal.append(cac)
+                cac = [a]
+        cal.append(cac)
+        
+        msg='Data layout not supported (only single non-transformed axis allowed)' 
+        if (len(cal)!=1):
+            raise HysopFFTDataLayoutError(msg)
+        baxes = cal[0]
+        
+        t_distances = strides[baxes]//dtype.itemsize
+                
+        if len(t_distances) == 0:
+            t_distance = 0
+        else:
+            t_distance = t_distances[0] 
+                       
+        batchsize = np.prod(shape[baxes])
+        
+        t_shape   = shape[taxes]
+        t_strides = strides[taxes]//dtype.itemsize
+        
+        return (tuple(t_strides), t_distance, batchsize, tuple(t_shape), tuple(taxes))
+        
+    @classmethod
+    def compute_input_array_offset(cls, real_input, fake_input, axes, 
+            transform_offset='K', idx='k{}', batch_id='b',
+            void_ptr='input',  casted_ptr='in'):
+        
+        new_input    = cls.extract_array(real_input)
+        input_offset = cls.get_array_offset(new_input, emit_warning=False)
+        input_data = new_input.base_data
+        input_fp   = dtype_to_ctype(new_input.dtype)
+        
+        offset_input_pointer = \
+                cls.compute_pointer_offset(real_array=real_input, fake_array=fake_input,
+                    axes=axes, base_offset=input_offset,
+                    transform_offset=transform_offset, idx=idx, batch_id=batch_id, 
+                    fp='const '+input_fp, void_ptr=void_ptr, casted_ptr=casted_ptr,
+                    is_input=True)
+
+        return (input_data, input_fp, offset_input_pointer)
+    
+    @classmethod
+    def compute_output_array_offset(cls, real_output, fake_output, axes, 
+            transform_offset='K', idx='k{}', batch_id='b',
+            void_ptr='output',  casted_ptr='out'):
+        
+        new_output    = cls.extract_array(real_output)
+        output_offset = cls.get_array_offset(new_output, emit_warning=True)
+        output_data = new_output.base_data
+        output_fp   = dtype_to_ctype(new_output.dtype)
+        
+        offset_output_pointer = \
+                cls.compute_pointer_offset(real_array=real_output, fake_array=fake_output,
+                    axes=axes, base_offset=output_offset,
+                    transform_offset=transform_offset, idx=idx, batch_id=batch_id, 
+                    fp=output_fp, void_ptr=void_ptr, casted_ptr=casted_ptr,
+                    is_input=False)
+
+        return (output_data, output_fp, offset_output_pointer)
+    
+    @classmethod
+    def compute_pointer_offset(cls, real_array, fake_array, 
+            axes, base_offset,
+            transform_offset, idx, batch_id, 
+            fp, void_ptr, casted_ptr,
+            is_input):
+
+        fake_strides, fake_distance, fake_batchsize, fake_shape, fake_axes = \
+                cls.calculate_transform_strides(axes, fake_array)
+        assert len(fake_shape) == len(fake_strides) <= 3
+        ndim = len(fake_shape)
+
+        K = transform_offset
+        k = idx
+        b = batch_id
+        vptr = void_ptr
+        cptr = casted_ptr
+        D = fake_distance
+        S = fake_strides[::-1]
+        
+        oip = ()
+        oip += ('uint {K} = offset;'.format(K=K),)
+        if (fake_batchsize > 1):
+            oip += ('const uint b = {K}/{D};'.format(K=K, D=D),)
+            # FIX ANOTHER CLFFT BUG (wrong batch size scheduling...)
+            if is_input:
+                oip += ('if (b>={}) {{ return ({})(NAN); }};'.format(fake_batchsize, fp),)
+                #oip += ('printf("\\noffset=%u", offset);',)
+            else:
+                oip += ('if (b>={}) {{ return; }};'.format(fake_batchsize, fp),)
+            oip += ('{K} -= {b}*{D};'.format(K=K, b=b, D=D),)
+        for i in xrange(ndim-1,-1,-1):
+            Ki = idx.format('xyz'[i])
+            Si = S[i]
+            oip += ('const uint {Ki} = {K}/{Si};'.format(Ki=Ki, K=K, Si=Si),)
+            if (i>0):
+                oip += ('{K} -= {Ki}*{Si};'.format(K=K, Ki=Ki, Si=Si),)
+        
+        if (real_array is fake_array):
+            offset = '{base_offset} + offset - {k}'.format(base_offset=base_offset,
+                                                            K=K, k=k.format('x'))
+        else:
+            real_strides, real_distance, real_batchsize, real_shape, real_axes = \
+                    cls.calculate_transform_strides(axes, real_array)
+            assert fake_batchsize == real_batchsize
+            assert np.array_equal(fake_axes, real_axes)
+            assert len(real_shape) == len(real_strides) == ndim
+            real_offset = ('{base_offset}uL'.format(base_offset=base_offset),)
+            if (fake_batchsize > 1):
+                real_offset += ('{b}*{D}'.format(b=b, D=real_distance),)
+            for i in xrange(ndim-1,0,-1):
+                Ki = idx.format('xyz'[i])
+                Si = real_strides[i]
+                real_offset += ('{Ki}*{Si}'.format(Ki, Si),)
+            offset = ' + '.join(real_offset)
+            
+        oip += ('__global {fp}* {cptr} = (__global {fp}*)({vptr}) + {offset};'.format(
+                        cptr=cptr, vptr=vptr,
+                        fp=fp, offset=offset),)
+        indent = ' '*12
+        offset_pointer = indent+'\n{}'.format(indent).join(oip)
+        return offset_pointer
+
+    @classmethod
+    def extract_array(cls, array):
+        offset    = (array.offset // array.dtype.itemsize)
+        alignment = (array.offset % array.dtype.itemsize)
+        assert (alignment == 0), 'Wrong array alignment.'
+        try:
+            # if offset is aligned on device memory boundary we may be
+            # able to create a subbuffer, else we keep original array.
+            data = array.data
+            new_array = array[offset:]
+        except:
+            new_array = array
+        return new_array
+    
+    @classmethod
+    def get_array_offset(cls, array, emit_warning):
+        """
+        Get array offset in terms of array elements, and emit a warning is offset is non
+        zero and if emit_warning is set.
+        """
+
+        dtype = array.dtype
+        if (array.offset % dtype.itemsize) != 0:
+            msg='Unaligned array offset.'
+            raise RuntimeError(msg)
+        base_offset = (array.offset // dtype.itemsize)
+        if emit_warning and (base_offset != 0):
+            msg='OpenCl array offset is not zero and will be injected into a clFFT pre or '
+            msg+= 'post callback. This could entail bad results if this buffer is used as '
+            msg+= 'an output: the beginning of this buffer may be used as a temporary ' 
+            msg+= 'buffer during the transform before actual results are stored at the right '
+            msg+= 'offset through the callback.'
+            warnings.warn(msg, HysopGpyFftWarning)
+        return base_offset
+
     def bake(self, queue=None):
+        """Bake the plan."""
         if self._baked:
             msg='Plan was already baked.'
             raise RuntimeError(msg)
+        def fmt_arg(name):
+            return self._setup_kwds[name]
+        def fmt_array(name):
+            arr = fmt_arg(name)
+            return 'shape={:<16} strides={:<16} dtype={:<16}'.format(
+                    str(arr.shape)+',', 
+                    str(arr.strides)+',',
+                    arr.dtype)
+        title=' Baking {} '.format(self.__class__.__name__) 
+        msg = \
+        '''    in_array:          {}
+    out_array:         {}
+    fake_input:        {}
+    fake_output:       {}
+    axes:              {}
+    direction_forward: {}
+    hardcode twiddles: {}'''.format(
+            fmt_array('in_array'),
+            fmt_array('out_array'),
+            fmt_array('fake_input'),
+            fmt_array('fake_output'),
+            fmt_arg('axes'),
+            fmt_arg('direction_forward'),
+            fmt_arg('hardcode_twiddles'))
+        if self.verbose:
+            print
+            print framed_str(title, msg, c='*')
+        self.plan.bake(self.queue)
         queue = first_not_None(queue, self.queue)
         self.plan.bake(queue)
         self._baked = True
         return self
 
     def allocate(self, buf=None):
+        """Allocate plan extra memory, possibly with a custom buffer."""
         if self._allocated:
             msg='Plan was already allocated.'
             raise RuntimeError(msg)
         size = self.plan.temp_array_size
         if (size>0):
             if (buf is None):
-                if self.warn_on_allocation:
+                if self.warn_on_allocation or self.error_on_allocation:
                     msg='Allocating temporary buffer of size {} for clFFT::{}.'
                     msg=msg.format(bytes2str(size), id(self))
-                    warnings.warn(msg, HysopWarning)
+                    if self.error_on_allocation:
+                        raise RuntimeError(msg)
+                    else:
+                        warnings.warn(msg, HysopGpyFftWarning)
                 buf = cl.Buffer(self.context, cl.mem_flags.READ_WRITE, size=size)
                 self.temp_buffer = buf
             elif (buf.size != size):
@@ -230,16 +674,16 @@ class GpyFFTPlan(FFTPlanI, FFTPlan):
         self._allocated = True
         return self
 
-    def enqueue(self, queue=None, wait_for_events=None):
+    def enqueue(self, queue=None, wait_for=None):
         """
         Enqueue transform with array base_data.
         """
+        queue = first_not_None(queue, self.queue)
         if not self._baked:
             self.bake(queue)
         if not self._allocated:
             self.allocate()
 
-        queue = first_not_None(queue, self.queue)
         in_data, out_data = self.in_data, self.out_data
         direction_forward = self.direction_forward
 
@@ -248,13 +692,13 @@ class GpyFFTPlan(FFTPlanI, FFTPlan):
                                             (in_data,), 
                                             direction_forward=direction_forward, 
                                             temp_buffer=self.temp_buffer, 
-                                            wait_for_events=wait_for_events)
+                                            wait_for_events=wait_for)
         else:
             events = self.plan.enqueue_transform((queue,), 
                                             (in_data,), (out_data),
                                             direction_forward=direction_forward, 
                                             temp_buffer=self.temp_buffer, 
-                                            wait_for_events=wait_for_events)
+                                            wait_for_events=wait_for)
         evt, = events
         return evt
 
@@ -263,6 +707,8 @@ class GpyFFTPlan(FFTPlanI, FFTPlan):
         raise NotImplementedError(msg)
 
     def execute(self, **kwds):
+        if __KERNEL_DEBUG__ or __TRACE_KERNELS__:
+            print '  {}<<<>>>()'.format(self.__class__.__name__)
         return self.enqueue(**kwds)
 
     @property
@@ -290,112 +736,61 @@ class GpyFFTPlan(FFTPlanI, FFTPlan):
     def output_array(self):
         return self.out_array
 
-    @classmethod
-    def check_dtype(cls, dtype, layout):
-        if layout in (gfft.CLFFT_HERMITIAN_INTERLEAVED, gfft.CLFFT_COMPLEX_INTERLEAVED):
-            if not is_complex(dtype):
-                msg='Layout is {} but got array with dtype {}.'
-                msg=msg.format(layout, dtype)
-                raise RuntimeError(msg)
-        elif layout in (gfft.CLFFT_REAL,):
-            if not is_fp(dtype):
-                msg='Layout is CLFFT_REAL but got array with dtype {}.'
-                msg=msg.format(dtype)
-                raise RuntimeError(msg)
-        else:
-            msg='Unsupported data layout {}.'
-            msg=msg.format(layout)
-            raise NotImplementedError(msg)
-
-    def get_array_offset(self, array):
-        dtype = array.dtype
-        if (array.offset % dtype.itemsize) != 0:
-            msg='Unaligned array offset.'
-            raise RuntimeError(msg)
-        base_offset = (array.offset // dtype.itemsize)
-        return base_offset
-
-    def pre_offset_callback(self, in_array, layout_in, **kwds):
-        dtype = in_array.dtype
-        fp = dtype_to_ctype(dtype)
-        self.check_dtype(dtype, layout_in)
-        base_offset = self.get_array_offset(in_array)
+    def pre_offset_callback(self, offset_input_pointer, in_fp, **kwds):
+        """Default pre_offset_callback, just inject input array offset."""
         callback = \
-        '''{fp} pre_callback(const __global void* input,
-                 const uint offset,
-                 __global void* userdata) {{
-            __global {fp}* in = (__global {fp}*) input; 
-            return in[{base_offset}uL+offset];
-        }}'''.format(fp=fp, base_offset=base_offset)
-
+        '''{fp} pre_callback(const __global void* input, const uint offset,
+                                   __global void* userdata) {{
+{offset_input_pointer}
+            return in[kx];
+        }}'''.format(fp=in_fp, offset_input_pointer=offset_input_pointer)
         return callback, None
-
-    def post_offset_callback(self, out_array, layout_out, **kwds):
-        dtype = out_array.dtype
-        self.check_dtype(dtype, layout_out)
-        fp = dtype_to_ctype(dtype)
-        base_offset = self.get_array_offset(out_array)
-
+    
+    def pre_offset_callback_C2R(self, offset_input_pointer, fp, N, **kwds):
+        """
+        C2R specific pre_offset_callback, inject input array offset 
+        and force the nyquist frequency to be purely real (fixes a bug
+        in clfft for even C2R transform of dimension > 1).
+        """
+        force_real_input = '(kx==0)'
+        if (N%2==0): # Nyquist freq
+            force_real_input += '|| (kx=={n})'.format(n=N//2)
         callback = \
-        '''void post_callback(__global void* output,
-                  const uint offset,
-                  __global void* userdata,
-                  const {fp} fftoutput) {{
-            __global {fp}* out = (__global {fp}*) output; 
-            out[{base_offset}uL+offset] = fftoutput;
-        }}'''.format(fp=fp, base_offset=base_offset)
-
+        '''{fp}2 pre_callback(const __global void* input, const uint offset,
+                                    __global void* userdata) {{
+{offset_input_pointer}
+            if ({force_real_input}) {{
+                return ({fp}2)(in[kx].x, 0);
+            }}
+            else {{
+                return in[kx];
+            }}
+        }}'''.format(fp=fp, force_real_input=force_real_input,
+                offset_input_pointer=offset_input_pointer)
         return callback, None
 
-    def allocate_plans(cls, backend, plans):
-        tmp_size = max(plan.required_buffer_size for plan in plans)
-
-        if (tmp_size>0):
-            msg='Allocating an additional {} temporary buffer for clFFT.'
-            msg=msg.format(bytes2str(tmp_size))
-            vprint(msg)
-            tmp_buffer = backend.empty(shape=(tmp_size), dtype=npw.uint8)
-            for plan in plans:
-                if (plan.required_buffer_size > tmp_buffer.nbytes):
-                    msg='\nFATAL ERROR: Failed to allocate temporary buffer for clFFT.'
-                    msg+='\n => clFFT expected {} bytes but only {} bytes have been allocated.\n'
-                    msg=msg.format(plan.required_buffer_size, tmp_buffer.nbytes)
-                    raise RuntimeError(msg)
-                else:
-                    buf = tmp_buffer[:plan.required_buffer_size]
-                    plan.allocate(buf=buf)
-        else:
-            for plan in plans:
-                assert plan.required_buffer_size == 0
-                plan.allocate()
-            tmp_buffer = None
-        return tmp_buffer
-
-
-class GpyR2RPlan(GpyFFTPlan):
-    """Specialization for real to real transforms built from r2c or c2r transforms."""
-
-    def __init__(self, in_array, out_array, scale_by_size, **kwds):
+    def post_offset_callback(self, offset_output_pointer, out_fp, S, **kwds):
         """
-        Handmade R2R transforms rely on a fake output that will
-        never really be written. This output is necessary because
-        clFFT use it as a temporary storage during the FFT computation.
-
-        The real output array will be passed as user data in a post callback.
+        Default post_offset_callback, just inject output array offset and scale by size 
+        (divide by some integer, which will often be the logical size of the
+        transform or 1).
         """
-        msg='Incompatible shapes {} vs {}.'.format(in_array.shape, out_array.shape)
-        assert np.array_equal(in_array.shape, out_array.shape), msg
-        msg='Incompatible dtypes {} vs {}.'.format(in_array.dtype, out_array.dtype)
-        assert (in_array.dtype == out_array.dtype)
-        msg='Only single and double precision are supported, got {}.'.format(in_array.dtype)
-        assert in_array.dtype in (np.float32, np.float64), msg
-        
-        scale_by_size = first_not_None(scale_by_size, 1)
-        super(GpyR2RPlan, self).__init__(in_array=in_array, out_array=out_array, 
-                scale_by_size=scale_by_size, **kwds)
-
+        callback = \
+        '''void post_callback(__global void* output, const uint offset,
+                              __global void* userdata, const {fp} R) {{
+{offset_output_pointer}
+            out[kx] = R / {S};
+        }}'''.format(fp=out_fp, offset_output_pointer=offset_output_pointer, S=S)
+        return callback, None
+    
+    
     @classmethod
     def fake_array(cls, shape, dtype, strides=None):
+        """
+        Create a fake_array of given shape and dtype.
+        If not given, the strides are computed from shape and dtype as if the array
+        would be contiguous in memory.
+        """
         class DummyArray(object):
             def __init__(self, shape, strides, dtype):
                 assert (shape is not None)
@@ -407,6 +802,7 @@ class GpyR2RPlan(GpyFFTPlan):
                 self.shape = shape
                 self.strides = strides
                 self.dtype = dtype
+                self.ndim = len(shape)
 
             @classmethod
             def compute_strides(cls, shape, dtype):
@@ -414,372 +810,458 @@ class GpyR2RPlan(GpyFFTPlan):
                 strides[1:] = np.cumprod(strides[:-1])
                 strides[0] = 1
                 strides = tuple(x*dtype.itemsize for x in strides)
-                return strides
+                return strides[::-1]
 
         array = DummyArray(shape=shape, strides=strides, dtype=dtype)
         return array
 
-    def generate_twiddles(self, name, base, count, typegen, dtype):
-        ctype = float_to_complex_dtype(dtype)
-        fp = dtype_to_ctype(dtype)
-        K = np.arange(count)
+    @classmethod
+    def generate_twiddles(cls, name, base, count, typegen, fp,
+            hardcode_twiddles, idx='kx', Tvar='T'):
+        """
+        Generate twiddles as a string.
+        OpenCl __constant static array: exp(base*k0) for k in 0..count
+        """
+        if hardcode_twiddles:
+            k0 = np.arange(count)
+            E = np.exp(1.0j*base*k0, dtype=np.complex128)
+            base = '\t\t({fp}2)({}, {})'
+            vals = ',\n'.join(base.format(
+                typegen.dump(x.real), typegen.dump(x.imag), fp=fp) for x in E)
+            twiddles = \
+            '''
+            __constant const {fp}2 {name}[{N}] = {{
+    {vals}
+            }};
+            '''.format(fp=fp, name=name, vals=vals, N=count)
+            twiddle = 'const {fp}2 {Tvar} = {name}[{idx}];'
+            twiddle = twiddle.format(fp=fp, Tvar=Tvar, name=name, idx=idx)
+        else:
+            twiddle = 'const {fp}2 {Tvar} = ({fp}2)(cos({base}*{idx}), sin({base}*{idx}));'
+            twiddle = twiddle.format(fp=fp, Tvar=Tvar, idx=idx, base=typegen.dump(base))
+            twiddles = ''
+        return (twiddle, twiddles)
 
-        E = np.exp(base*K, dtype=np.complex128)
-        base = '\t\t({fp}2)({}, {})'
-        vals = ',\n'.join(base.format(
-            typegen.dump(x.real), typegen.dump(x.imag), fp=fp) for x in E)
-        twiddles = \
-        '''
-        __constant const {fp}2 {name}[{N}] = {{
-{vals}
-        }};
-        '''.format(fp=fp, name=name, vals=vals, N=count)
-        return twiddles;
 
-    @abstractmethod
-    def setup_plan(self, in_array, out_array, axes, direction_forward, scaling, scale_by_size):
-        """Redefine plan creation behaviour."""
-        pass
+class GpyR2RPlan(GpyFFTPlan):
+    """
+    Specialization for real to real transforms built from r2c or c2r transforms.
+    Real to real transforms use fake arrays as input and output along with
+    custom pre and post processing callbacks.
+    """
 
+    def __init__(self, in_array, out_array, 
+            fake_input, fake_output, 
+            scale_by_size, axes, 
+            **kwds):
+        """
+        Handmade R2R transforms rely on fake input and output that will
+        never really be read or written. This is necessary because
+        clFFT do not handle R2R transforms and we use pre and post processing
+        to compute an equivalent R2C or C2R problem.
+        
+        Fake arrays are used to compute transform size, batch size and strides.
+        Real arrays pointer are passed to the kernels and pre and post callbacks
+        map the input and output data from those real arrays, adjusting the stride
+        computations to the real array sizes from the fake array indices.
+        """
+        real_types = (np.float32, np.float64)
+        msg='Incompatible shapes {} vs {}.'.format(in_array.shape, out_array.shape)
+        assert np.array_equal(in_array.shape, out_array.shape), msg
+        msg='Incompatible dtypes {} vs {}.'.format(in_array.dtype, out_array.dtype)
+        assert (in_array.dtype == out_array.dtype), msg
+        msg='Incompatible dtype {}, expected {}.'.format(in_array.dtype, real_types)
+        assert (in_array.dtype in real_types), msg
+        msg='Fake input has not been set.'
+        assert (fake_input is not None), msg
+        msg='Fake output has not been set.'
+        assert (fake_output is not None), msg
+        
+        axis = self.check_r2r_axes(in_array, axes)
+        axes = np.asarray([axis])
+        
+        super(GpyR2RPlan, self).__init__(in_array=in_array, out_array=out_array, 
+                fake_input=fake_input, fake_output=fake_output,
+                axes=axes, scale_by_size=scale_by_size, **kwds)
 
-class GpyDCTIPlan(GpyR2RPlan):
-    def setup_plan(self, in_array, out_array, axes, direction_forward, scaling, scale_by_size):
-        assert in_array.data != out_array.data, 'inplace R2R transforms are not supported.'
+    def setup_plan(self, **kwds):
+        super(GpyR2RPlan, self).setup_plan(**kwds)
+        if self.is_inplace:
+            msg='R2R transforms cannot be compute inplace on this backend.'
+            raise NotImplementedError(msg)
+    
+    @classmethod
+    def prepare_r2r(cls, in_array, axes):
+        """Return all the required variables to build fake arrays for a all R2R transforms."""
+        axis   = cls.check_r2r_axes(in_array, axes)
+        shape  = in_array.shape
+        N      = shape[axis]
+        dtype  = in_array.dtype
+        ctype  = float_to_complex_dtype(dtype)
+        return (dtype, ctype, shape, axis, N)
+    
+    @classmethod
+    def check_r2r_axes(cls, in_array, axes):
+        """Check that only the last axis is transformed."""
         axis = in_array.ndim - 1
         assert len(axes)==1
         assert axes[0] in (-1, axis)
-        axes = np.asarray([axis])
+        return axis
 
-        assert scale_by_size > 0
 
-        # build a fake R2C plan
-        layout_in  = gfft.CLFFT_REAL
-        layout_out = gfft.CLFFT_HERMITIAN_INTERLEAVED
+class GpyDCTIPlan(GpyR2RPlan):
 
-        shape  = in_array.shape
-        dtype  = in_array.dtype
-        fp = dtype_to_ctype(dtype)
-        N  = shape[axis]
+    def __init__(self, in_array, axes, **kwds):
+        (dtype, ctype, shape, axis, N) = self.prepare_r2r(in_array, axes)
         rshape = mk_shape(shape, axis, 2*N-2)
         cshape = mk_shape(shape, axis, N)
-        ctype  = float_to_complex_dtype(dtype)
-
         fake_input  = self.fake_array(shape=rshape, dtype=dtype)
         fake_output = self.fake_array(shape=cshape, dtype=ctype)
+        super(GpyDCTIPlan, self).__init__(in_array=in_array, axes=axes,
+                fake_input=fake_input, fake_output=fake_output, **kwds)
 
-        t_strides_in, t_distance_in, t_batchsize_in, t_shape_in, axes_transform_in = \
-                self.calculate_transform_strides(axes, fake_input)
-        t_strides_out, t_distance_out, t_batchsize_out, t_shape_out, axes_transform_out = \
-                self.calculate_transform_strides(axes, fake_output)
+    def pre_offset_callback(self, N, fp, offset_input_pointer, **kwds):
+        pre = \
+        '''{fp} pre_callback(const __global void* input, const uint offset, 
+                                   __global void* userdata) {{
+{offset_input_pointer}
+            {fp} ret;
+            if (kx<{N}) {{
+                ret = in[kx];
+            }}
+            else {{
+                ret = in[2*{N}-kx-2];
+            }}
+            return ret;
+        }}'''.format(N=N, fp=fp, offset_input_pointer=offset_input_pointer)
+        return pre, None
 
-        assert np.array_equal(t_batchsize_in, t_batchsize_out)
-        assert np.array_equal(axes_transform_in, axes_transform_out)
-        t_shape      = t_shape_in
-        t_batch_size = t_batchsize_in
-        t_inplace    = False
-
-        plan = GFFT.create_plan(self.context, t_shape)
-        plan.inplace     = t_inplace
-        plan.batch_size  = t_batch_size
-        plan.strides_in  = t_strides_in
-        plan.strides_out = t_strides_out
-        plan.distances   = (t_distance_in, t_distance_out)
-        plan.layouts     = (layout_in, layout_out)
-        if (dtype == np.float32):
-            plan.precision = gfft.CLFFT_SINGLE
-        else:
-            plan.precision = gfft.CLFFT_DOUBLE
-        assert scaling is None
-        if (scaling is not None):
-            if direction_forward:
-                plan.scale_forward = scaling
-            else:
-                plan.scale_backward = scaling
+    def post_offset_callback(self, fp, S, offset_output_pointer, **kwds):
+        post = \
+        '''void post_callback(__global void* output, const uint offset,
+                              __global void* userdata, const {fp}2 R) {{
+{offset_output_pointer}
+            out[kx] = R.x/{S};
+        }}'''.format(S=S, fp=fp, offset_output_pointer=offset_output_pointer)
+        return post, None
 
-        (in_data, out_data) = self.set_callbacks(plan, in_array, out_array, 
-                                                       layout_in, layout_out,
-                                                       N=N, fp=fp, S=scale_by_size)
 
-        self.in_data    = in_data
-        self.out_data   = out_data
-        self.is_inplace = t_inplace
-        self.direction_forward = direction_forward
-        self.plan = plan
+class GpyDCTIIPlan(GpyR2RPlan):
+    def __init__(self, in_array, axes, **kwds):
+        (dtype, ctype, shape, axis, N) = self.prepare_r2r(in_array, axes)
+        rshape = mk_shape(shape, axis, N)
+        cshape = mk_shape(shape, axis, N//2+1)
+        fake_input  = self.fake_array(shape=rshape, dtype=dtype)
+        fake_output = self.fake_array(shape=cshape, dtype=ctype)
+        super(GpyDCTIIPlan, self).__init__(in_array=in_array, axes=axes,
+                fake_input=fake_input, fake_output=fake_output, **kwds)
 
-    def pre_offset_callback(self, in_array, layout_in, N, fp, S):
-        base_offset = self.get_array_offset(in_array)
+    def pre_offset_callback(self, N, fp, offset_input_pointer, **kwds):
+        n = (N-1)//2 + 1
         pre = \
-        '''{fp} pre_callback(const __global void* input, uint k,
-                 __global void* userdata) {{
-            __global {fp}* in = (__global {fp}*)(input) + {base_offset}uL;
-            in += k/(2*{N}-2) * {N};
+        '''
+        {fp} pre_callback(const __global void* input, uint offset, 
+                                __global void* userdata) {{
+{offset_input_pointer}
             {fp} ret;
-            k = k%(2*{N}-2);
-            if (k<{N}) {{
-                ret = in[k];
+            if (kx<{n}) {{
+                ret = in[2*kx];
             }}
             else {{
-                ret = in[2*{N}-k-2];
+                ret = in[2*({N}-kx)-1];
             }}
             return ret;
-        }}'''.format(N=N, fp=fp, base_offset=base_offset)
+        }}'''.format(n=n, N=N, fp=fp, offset_input_pointer=offset_input_pointer)
         return pre, None
 
-    def post_offset_callback(self, out_array, layout_out, N, fp, S):
-        base_offset = self.get_array_offset(out_array)
+    def post_offset_callback(self, N, S, fp, offset_output_pointer,
+            typegen, hardcode_twiddles, **kwds):
+        n = (N-1)//2 + 1
+        (twiddle, twiddles) = self.generate_twiddles('dct2_twiddles', 
+                                            base=-np.pi/(2*N), count=N//2+1,
+                                            fp=fp, typegen=typegen,
+                                            hardcode_twiddles=hardcode_twiddles)
         post = \
-        '''void post_callback(__global void* output, uint k,
-                  __global void* userdata, {fp}2 fftoutput) {{
-            __global {fp}* out = (__global {fp}*)(output) + {base_offset}uL;
-            out[k] = fftoutput.x/{S};
-        }}'''.format(N=N, S=S, fp=fp, base_offset=base_offset)
+        '''
+        {twiddles}
+        void post_callback(__global void* output, const uint offset, 
+                           __global void* userdata, const {fp}2 R) {{
+            {offset_output_pointer}
+            {twiddle}
+            if (kx < {n}) {{
+                out[kx] = +2*(R.x*T.x - R.y*T.y)/{S};
+            }}
+            if (kx > 0) {{
+                out[{N}-kx] = -2*(R.x*T.y + R.y*T.x)/{S};
+            }}
+        }}'''.format(N=N, S=S, n=n, fp=fp, 
+                twiddle=twiddle, twiddles=twiddles,
+                offset_output_pointer=offset_output_pointer)
         return post, None
 
 
-class GpyDCTIIPlan(GpyR2RPlan):
-    def setup_plan(self, in_array, out_array, axes, direction_forward, scaling, scale_by_size):
-        assert in_array.data != out_array.data, 'inplace R2R transforms are not supported.'
-        axis = in_array.ndim - 1
-        assert len(axes)==1
-        assert axes[0] in (-1, axis)
-        axes = np.asarray([axis])
-        
-        assert scale_by_size > 0
-
-        # build a fake R2C plan
-        layout_in  = gfft.CLFFT_REAL
-        layout_out = gfft.CLFFT_HERMITIAN_INTERLEAVED
-
-        shape  = in_array.shape
-        dtype  = in_array.dtype
-        fp = dtype_to_ctype(dtype)
-        N  = shape[axis]
+class GpyDCTIIIPlan(GpyR2RPlan):
+    def __init__(self, in_array, axes, **kwds):
+        (dtype, ctype, shape, axis, N) = self.prepare_r2r(in_array, axes)
         rshape = mk_shape(shape, axis, N)
         cshape = mk_shape(shape, axis, N//2+1)
-        ctype  = float_to_complex_dtype(dtype)
+        fake_input  = self.fake_array(shape=cshape, dtype=ctype)
+        fake_output = self.fake_array(shape=rshape, dtype=dtype)
+        super(GpyDCTIIIPlan, self).__init__(in_array=in_array, axes=axes,
+                fake_input=fake_input, fake_output=fake_output, **kwds)
+    
+    def pre_offset_callback(self, **kwds):
+        msg='pre_offset_callback_C2R should be used instead.'
+        raise NotImplementedError(msg)
+
+    def pre_offset_callback_C2R(self, N, S, fp, typegen, 
+            offset_input_pointer, hardcode_twiddles, **kwds):
+        (twiddle, twiddles) = self.generate_twiddles('dct3_twiddles', 
+                                            base=+np.pi/(2*N), count=N//2+1,
+                                            fp=fp, typegen=typegen,
+                                            hardcode_twiddles=hardcode_twiddles)
+        force_real_input = '(kx==0)'
+        if (N%2==0): # Nyquist freq
+            force_real_input += '|| (kx=={n})'.format(n=N//2)
+        pre = \
+        '''
+        {twiddles}
+        {fp}2 pre_callback(const __global void* input, const uint offset, 
+                                 __global void* userdata) {{
+            {offset_input_pointer}
+            {twiddle}
+            {fp}2 C, R;
+            R.x = in[kx];
+            if (kx==0) {{
+                R.y = 0;
+            }}
+            else {{
+                R.y = -in[{N}-kx];
+            }}
+            C.x = R.x*T.x - R.y*T.y;
+            if ({force_real_input}) {{
+                C.y = 0;
+            }}
+            else {{
+                C.y = R.x*T.y + R.y*T.x;
+            }}
+            return C;
+        }}'''.format(N=N, fp=fp, 
+                offset_input_pointer=offset_input_pointer,
+                twiddle=twiddle, twiddles=twiddles,
+                force_real_input=force_real_input)
+        return pre, None
+
+    def post_offset_callback(self, N, S, fp, 
+            offset_output_pointer, **kwds):
+        n = (N-1)//2 + 1
+        post = \
+        '''
+        void post_callback(__global void* output, const uint offset, 
+                           __global void* userdata, const {fp} R) {{
+            {offset_output_pointer}
+            if (kx < {n}) {{
+                out[2*kx] = R/{S};
+            }}
+            else {{
+                out[2*({N}-kx)-1] = R/{S};
+            }}
+        }}'''.format(N=N, S=S, n=n, fp=fp, 
+                        offset_output_pointer=offset_output_pointer)
+        return post, None
 
+
+class GpyDSTIPlan(GpyR2RPlan):
+
+    def __init__(self, in_array, axes, **kwds):
+        (dtype, ctype, shape, axis, N) = self.prepare_r2r(in_array, axes)
+        rshape = mk_shape(shape, axis, 2*N+2)
+        cshape = mk_shape(shape, axis, N+2)
         fake_input  = self.fake_array(shape=rshape, dtype=dtype)
         fake_output = self.fake_array(shape=cshape, dtype=ctype)
+        super(GpyDSTIPlan, self).__init__(in_array=in_array, axes=axes,
+                fake_input=fake_input, fake_output=fake_output, **kwds)
 
-        t_strides_in, t_distance_in, t_batchsize_in, t_shape_in, axes_transform_in = \
-                self.calculate_transform_strides(axes, fake_input)
-        t_strides_out, t_distance_out, t_batchsize_out, t_shape_out, axes_transform_out = \
-                self.calculate_transform_strides(axes, fake_output)
-
-        assert np.array_equal(t_batchsize_in, t_batchsize_out)
-        assert np.array_equal(axes_transform_in, axes_transform_out)
-        t_shape      = t_shape_in
-        t_batch_size = t_batchsize_in
-        t_inplace    = False
-
-        plan = GFFT.create_plan(self.context, t_shape)
-        plan.inplace     = t_inplace
-        plan.batch_size  = t_batch_size
-        plan.strides_in  = t_strides_in
-        plan.strides_out = t_strides_out
-        plan.distances   = (t_distance_in, t_distance_out)
-        plan.layouts     = (layout_in, layout_out)
-        if (dtype == np.float32):
-            plan.precision = gfft.CLFFT_SINGLE
-            precision = Precision.FLOAT
-        else:
-            plan.precision = gfft.CLFFT_DOUBLE
-            precision = Precision.DOUBLE
-        if (scaling is not None):
-            if direction_forward:
-                plan.scale_forward  = scaling
-            else:
-                plan.scale_backward = scaling
+    def pre_offset_callback(self, N, fp, offset_input_pointer, **kwds):
+        pre = \
+        '''{fp} pre_callback(const __global void* input, const uint offset, 
+                                   __global void* userdata) {{
+{offset_input_pointer}
+            {fp} ret;
+            if ((kx==0) || (kx=={N}+1)) {{
+                ret = 0;
+            }}
+            else if (kx<{N}+1) {{
+                ret = -in[kx-1];
+            }}
+            else {{
+                ret = +in[2*{N}+1-kx];
+            }}
+            return ret;
+        }}'''.format(N=N, fp=fp, offset_input_pointer=offset_input_pointer)
+        return pre, None
 
-        typegen = self.cl_env.build_typegen(precision=precision,
-                float_dump_mode='hex', use_short_circuit_ops=False, unroll_loops=False) 
+    def post_offset_callback(self, fp, N, S, offset_output_pointer, **kwds):
+        post = \
+        '''void post_callback(__global void* output, const uint offset,
+                              __global void* userdata, const {fp}2 R) {{
+{offset_output_pointer}
+            if ((kx!=0) && (kx!={N}+1)) {{
+                out[kx-1] = R.y/{S};
+            }}
+        }}'''.format(N=N, S=S, fp=fp, offset_output_pointer=offset_output_pointer)
+        return post, None
 
-        (in_data, out_data) = self.set_callbacks(plan, in_array, out_array, 
-                                                       layout_in, layout_out,
-                                                       N=N, S=scale_by_size,
-                                                       fp=fp, typegen=typegen)
 
-        self.in_data    = in_data
-        self.out_data   = out_data
-        self.is_inplace = t_inplace
-        self.direction_forward = direction_forward
-        self.plan = plan
+class GpyDSTIIPlan(GpyR2RPlan):
+    def __init__(self, in_array, axes, **kwds):
+        (dtype, ctype, shape, axis, N) = self.prepare_r2r(in_array, axes)
+        rshape = mk_shape(shape, axis, N)
+        cshape = mk_shape(shape, axis, N//2+1)
+        fake_input  = self.fake_array(shape=rshape, dtype=dtype)
+        fake_output = self.fake_array(shape=cshape, dtype=ctype)
+        super(GpyDSTIIPlan, self).__init__(in_array=in_array, axes=axes,
+                fake_input=fake_input, fake_output=fake_output, **kwds)
 
-    def pre_offset_callback(self, in_array, layout_in, N, S, fp, typegen):
-        base_offset = self.get_array_offset(in_array)
+    def pre_offset_callback(self, N, fp, offset_input_pointer, **kwds):
         n = (N-1)//2 + 1
         pre = \
         '''
-        {fp} pre_callback(const __global void* input, uint k, __global void* userdata) {{
-            __global {fp}* in = (__global {fp}*)(input) + {base_offset}uL;
+        {fp} pre_callback(const __global void* input, uint offset, 
+                                __global void* userdata) {{
+{offset_input_pointer}
             {fp} ret;
-            if (k<{n}) {{
-                ret = in[2*k];
+            if (kx<{n}) {{
+                ret = +in[2*kx];
             }}
             else {{
-                ret = in[2*({N}-k)-1];
+                ret = -in[2*({N}-kx)-1];
             }}
             return ret;
-        }}'''.format(n=n, N=N, fp=fp, base_offset=base_offset)
+        }}'''.format(n=n, N=N, fp=fp, offset_input_pointer=offset_input_pointer)
         return pre, None
 
-    def post_offset_callback(self, out_array, layout_out, N, S, fp, typegen):
-        base_offset = self.get_array_offset(out_array)
+    def post_offset_callback(self, N, S, fp, offset_output_pointer,
+            typegen, hardcode_twiddles, **kwds):
         n = (N-1)//2 + 1
-        twiddles = self.generate_twiddles('dct2_twiddles', base=-1.0j*np.pi/(2*N), count=N//2+1,
-                                            dtype=out_array.dtype, typegen=typegen)
+        (twiddle, twiddles) = self.generate_twiddles('dst2_twiddles', 
+                                            base=-np.pi/(2*N), count=N//2+1,
+                                            fp=fp, typegen=typegen,
+                                            hardcode_twiddles=hardcode_twiddles)
         post = \
         '''
         {twiddles}
-        void post_callback(__global void* output, uint k, __global void* userdata, {fp}2 R) {{
-            __global {fp}* out = (__global {fp}*)(output) + {base_offset}uL;
-            //const {fp}2 T = ({fp}2)(cos(-pi*k/(2*N)), sin(-pi*k/(2*N)));
-            const {fp}2 T = dct2_twiddles[k];
-            if (k < {n}) {{
-                out[k] = +2*(R.x*T.x - R.y*T.y)/{S};
+        void post_callback(__global void* output, const uint offset, 
+                           __global void* userdata, const {fp}2 R) {{
+            {offset_output_pointer}
+            {twiddle}
+            if (kx > 0) {{
+                out[kx-1] = -2*(R.x*T.y + R.y*T.x)/{S};
             }}
-            if (k > 0) {{
-                out[{N}-k] = -2*(R.x*T.y + R.y*T.x)/{S};
+            if (kx < {n}) {{
+                out[{N}-kx-1] = +2*(R.x*T.x - R.y*T.y)/{S};
             }}
-        }}'''.format(N=N, S=S, n=n, fp=fp, base_offset=base_offset, 
-                twiddles=twiddles)
+        }}'''.format(N=N, S=S, n=n, fp=fp, 
+                twiddle=twiddle, twiddles=twiddles,
+                offset_output_pointer=offset_output_pointer)
         return post, None
 
 
-class GpyDCTIIIPlan(GpyR2RPlan):
-    def setup_plan(self, in_array, out_array, axes, direction_forward, scaling, scale_by_size):
-        assert in_array.data != out_array.data, 'inplace R2R transforms are not supported.'
-        axis = in_array.ndim - 1
-        assert len(axes)==1
-        assert axes[0] in (-1, axis)
-        axes = np.asarray([axis])
-
-        assert scale_by_size>0
-
-        # build a fake R2C plan
-        layout_in  = gfft.CLFFT_HERMITIAN_INTERLEAVED
-        layout_out = gfft.CLFFT_REAL
-
-        shape  = in_array.shape
-        dtype  = in_array.dtype
-        fp = dtype_to_ctype(dtype)
-        N  = shape[axis]
+class GpyDSTIIIPlan(GpyR2RPlan):
+    def __init__(self, in_array, axes, **kwds):
+        (dtype, ctype, shape, axis, N) = self.prepare_r2r(in_array, axes)
         rshape = mk_shape(shape, axis, N)
         cshape = mk_shape(shape, axis, N//2+1)
-        ctype  = float_to_complex_dtype(dtype)
-
         fake_input  = self.fake_array(shape=cshape, dtype=ctype)
         fake_output = self.fake_array(shape=rshape, dtype=dtype)
+        super(GpyDSTIIIPlan, self).__init__(in_array=in_array, axes=axes,
+                fake_input=fake_input, fake_output=fake_output, **kwds)
 
-        t_strides_in, t_distance_in, t_batchsize_in, t_shape_in, axes_transform_in = \
-                self.calculate_transform_strides(axes, fake_input)
-        t_strides_out, t_distance_out, t_batchsize_out, t_shape_out, axes_transform_out = \
-                self.calculate_transform_strides(axes, fake_output)
-
-        assert np.array_equal(t_batchsize_in, t_batchsize_out)
-        assert np.array_equal(axes_transform_in, axes_transform_out)
-        t_shape      = t_shape_out
-        t_batch_size = t_batchsize_in
-        t_inplace    = False
-
-        plan = GFFT.create_plan(self.context, t_shape)
-        plan.inplace     = t_inplace
-        plan.batch_size  = t_batch_size
-        plan.strides_in  = t_strides_in
-        plan.strides_out = t_strides_out
-        plan.distances   = (t_distance_in, t_distance_out)
-        plan.layouts     = (layout_in, layout_out)
-        if (dtype == np.float32):
-            plan.precision = gfft.CLFFT_SINGLE
-            precision = Precision.FLOAT
-        else:
-            plan.precision = gfft.CLFFT_DOUBLE
-            precision = Precision.DOUBLE
-        if (scaling is not None):
-            plan.scale_forward = scaling/float(N)
-            plan.scale_backward = scaling/float(N)
-
-        typegen = self.cl_env.build_typegen(precision=precision,
-                float_dump_mode='hex', use_short_circuit_ops=False, unroll_loops=False) 
-
-        (in_data, out_data) = self.set_callbacks(plan, in_array, out_array, 
-                                                       layout_in, layout_out,
-                                                       N=N, S=scale_by_size,
-                                                       fp=fp, typegen=typegen)
-
-        self.in_data    = in_data
-        self.out_data   = out_data
-        self.is_inplace = t_inplace
-        self.direction_forward = direction_forward
-        self.plan = plan
+    def pre_offset_callback(self, **kwds):
+        msg='pre_offset_callback_C2R should be used instead.'
+        raise NotImplementedError(msg)
 
-    def pre_offset_callback(self, in_array, layout_in, N, S, fp, typegen):
-        base_offset = self.get_array_offset(in_array)
-        twiddles = self.generate_twiddles('dct3_twiddles', base=+1.0j*np.pi/(2*N), count=N//2+1,
-                                            dtype=in_array.dtype, typegen=typegen)
+    def pre_offset_callback_C2R(self, N, S, fp, typegen, 
+            offset_input_pointer, hardcode_twiddles, **kwds):
+        (twiddle, twiddles) = self.generate_twiddles('dst3_twiddles', 
+                                            base=+np.pi/(2*N), count=N//2+1,
+                                            fp=fp, typegen=typegen,
+                                            hardcode_twiddles=hardcode_twiddles)
+        force_real_input = '(kx==0)'
+        if (N%2==0): # Nyquist freq
+            force_real_input += '|| (kx=={n})'.format(n=N//2)
         pre = \
         '''
         {twiddles}
-        {fp}2 pre_callback(const __global void* input, uint k, __global void* userdata) {{
-            __global {fp}* in = (__global {fp}*)(input) + {base_offset}uL;
-            const {fp}2 T = ({fp}2)(cos({pi}*k/(2*{N})), sin({pi}*k/(2*{N})));
-            //const {fp}2 T = dct3_twiddles[k];
+        {fp}2 pre_callback(const __global void* input, const uint offset, 
+                                 __global void* userdata) {{
+            {offset_input_pointer}
+            {twiddle}
             {fp}2 C, R;
-            R.x = in[k];
-            if ( k == 0 ) {{
+            R.x = in[{N}-kx-1];
+            if (kx==0) {{
                 R.y = 0;
             }}
             else {{
-                R.y = -in[{N}-k];
+                R.y = -in[kx-1];
             }}
             C.x = R.x*T.x - R.y*T.y;
-            C.y = R.x*T.y + R.y*T.x;
+            if ({force_real_input}) {{
+                C.y = 0;
+            }}
+            else {{
+                C.y = R.x*T.y + R.y*T.x;
+            }}
             return C;
-        }}'''.format(N=N, fp=fp, base_offset=base_offset,
-                pi=typegen.dump(np.pi), twiddles=twiddles)
+        }}'''.format(N=N, fp=fp, 
+                offset_input_pointer=offset_input_pointer,
+                twiddle=twiddle, twiddles=twiddles,
+                force_real_input=force_real_input)
         return pre, None
 
-    def post_offset_callback(self, out_array, layout_out, N, S, fp, typegen):
-        base_offset = self.get_array_offset(out_array)
+    def post_offset_callback(self, N, S, fp, 
+            offset_output_pointer, **kwds):
         n = (N-1)//2 + 1
         post = \
         '''
-        void post_callback(__global void* output, uint k, __global void* userdata, {fp} R) {{
-            __global {fp}* out = (__global {fp}*)(output) + {base_offset}uL;
-            if (k < {n}) {{
-                out[2*k] = {N}*R/{S};
+        void post_callback(__global void* output, const uint offset, 
+                           __global void* userdata, const {fp} R) {{
+            {offset_output_pointer}
+            if (kx < {n}) {{
+                out[2*kx] = +R/{S};
             }}
             else {{
-                out[2*({N}-k)-1] = {N}*R/{S};
+                out[2*({N}-kx)-1] = -R/{S};
             }}
-        }}'''.format(N=N, S=S, n=n, fp=fp, base_offset=base_offset)
+        }}'''.format(N=N, S=S, n=n, fp=fp, 
+                        offset_output_pointer=offset_output_pointer)
         return post, None
 
 
-
-class GpyDSTIPlan(GpyR2RPlan):
-    pass
-class GpyDSTIIPlan(GpyR2RPlan):
-    pass
-class GpyDSTIIIPlan(GpyR2RPlan):
-    pass
-
-
-class GpyFFT(FFTI):
+class GpyFFT(OpenClFFTI):
     """
     Interface to compute local to process FFT-like transforms using the clFFT backend
-    trough the gpyfft python interface.
+    through the gpyfft python interface.
 
     clFFT backend has many advantages: 
         - single and double precision supported
         - no intermediate temporary buffers created at each call.
         - all required temporary buffers can be supplied or are auto-allocated only once.
-        - planning capability but no caching capabilities
+        - real planning capability (but no explicit caching capabilities)
         - injection of custom opencl code for pre and post processing.
 
+    It also has some disadvantages:
+        - Bad OpenCL CPU devices support
+        - The library is to greedy with temporary buffers for big transforms.
+
     Planning may destroy initial arrays content.
     Executing a plan may result in unwanted writes to output data, see notes.
 
+    Note that custom code injection is not available for all transforms:
+        1) All real to real transforms are implemented using pre and post processing capabilities.
+        2) Pre and post processing is used to inject array base offsets.
+    User should take care of extending previously defined pre and post processing opencl code.
+
     Notes
     -----
     Output array is used during transform and if out.data is not aligned 
@@ -789,39 +1271,43 @@ class GpyFFT(FFTI):
     out.data = out.base_data + out.offset 
     if (offset%alignment > 0)
         out.base_data[0:out.size] 
-        will be trashed during computation and the result of the transform will go to 
+        may be trashed during computation and the result of the transform will go to 
         out.base_data[out.offset:out.offset+out.size]
 
-    Thus for every transforms out.base_data[0:min(out.offset,out.size)] may be overwritten with trash data.
-    The default behaviour is to warn when output data is not aligned on device memory boundary.
+    Thus for every transforms out.base_data[0:min(out.offset,out.size)] may be overwritten with 
+    trash data. The default behaviour is to emmit a warning when output data is not aligned on
+    device memory boundary.
     """
 
-    def __init__(self, cl_env, backend=None,
+    def __init__(self, cl_env, 
+                       backend=None, allocator=None,
                        warn_on_allocation=True,
-                       warn_on_unaligned_output_offset=True):
+                       warn_on_unaligned_output_offset=True, 
+                       error_on_allocation=False,
+                       **kwds):
 
-        if (backend is None):
-            backend = OpenClArrayBackend.get_or_create(cl_env=cl_env, 
-                            queue=cl_env.default_queue, allocator=None)
+        super(GpyFFT, self).__init__(cl_env=cl_env, 
+                backend=backend, allocator=allocator, 
+                warn_on_allocation=warn_on_allocation, 
+                error_on_allocation=error_on_allocation, **kwds)
 
-        super(GpyFFT, self).__init__(backend=backend)
         self.supported_ftypes = (np.float32, np.float64)
         self.supported_ctypes = (np.complex64, np.complex128)
         self.supported_cosine_transforms = (1,2,3)
-        self.supported_sine_transforms   = ()
-
-        self.cl_env  = cl_env
-        self.warn_on_allocation = warn_on_allocation
+        self.supported_sine_transforms   = (1,2,3)
         self.warn_on_unaligned_output_offset = warn_on_unaligned_output_offset
 
     def allocate_output(self, out, shape, dtype):
         """Alocate output if required and check shape and dtype."""
         if (out is None):
-            if self.warn_on_allocation:
+            if self.warn_on_allocation or self.error_on_allocation:
                 nbytes = prod(shape)*dtype.itemsize
                 msg='GpyFFT: allocating output array of size {}.'
                 msg=msg.format(bytes2str(nbytes))
-                warnings.warn(msg, HysopGpyFftWarning)
+                if self.error_on_allocation:
+                    raise RuntimeError(msg)
+                else:
+                    warnings.warn(msg, HysopGpyFftWarning)
             out = self.backend.empty(shape=shape, dtype=dtype)
         else:
             assert out.dtype == dtype
@@ -830,14 +1316,15 @@ class GpyFFT(FFTI):
 
     def bake_kwds(self, **kwds):
         plan_kwds = {}
-        plan_kwds['in_array']           = kwds.pop('a')
-        plan_kwds['out_array']          = kwds.pop('out')
-        plan_kwds['scaling']            = kwds.pop('scaling', None)
-        plan_kwds['scale_by_size']      = kwds.pop('scale_by_size', None)
-        plan_kwds['direction_forward']  = kwds.pop('direction_forward', True)
-        plan_kwds['axes']               = kwds.pop('axes', (kwds.pop('axis'),))
-        plan_kwds['cl_env']             = kwds.pop('cl_env',  self.cl_env)
-        plan_kwds['warn_on_allocation'] = kwds.pop('warn_on_allocation', self.warn_on_allocation)
+        plan_kwds['in_array']            = kwds.pop('a')
+        plan_kwds['out_array']           = kwds.pop('out')
+        plan_kwds['scaling']             = kwds.pop('scaling', None)
+        plan_kwds['scale_by_size']       = kwds.pop('scale_by_size', None)
+        plan_kwds['axes']                = kwds.pop('axes', (kwds.pop('axis'),))
+        plan_kwds['cl_env']              = kwds.pop('cl_env',  self.cl_env)
+        plan_kwds['verbose']             = kwds.pop('verbose', __VERBOSE__)
+        plan_kwds['warn_on_allocation']  = kwds.pop('warn_on_allocation',  self.warn_on_allocation)
+        plan_kwds['error_on_allocation'] = kwds.pop('error_on_allocation', self.error_on_allocation)
         plan_kwds['warn_on_unaligned_output_offset'] = \
                 kwds.pop('warn_on_unaligned_output_offset', 
                         self.warn_on_unaligned_output_offset)
@@ -857,10 +1344,10 @@ class GpyFFT(FFTI):
         return plan
 
     def ifft(self, a, out=None, axis=-1, **kwds):
-        (shape, dtype) = super(GpyFFT, self).ifft(a=a, out=out, axis=axis, **kwds)
+        (shape, dtype, s) = super(GpyFFT, self).ifft(a=a, out=out, axis=axis, **kwds)
         out = self.allocate_output(out, shape, dtype) 
-        kwds = self.bake_kwds(a=a, out=out, axis=axis, direction_forward=False, **kwds)
-        plan = GpyFFTPlan(**kwds)
+        kwds = self.bake_kwds(a=a, out=out, axis=axis, scaling='DEFAULT', **kwds)
+        plan = GpyFFTPlan(direction_forward=False, **kwds)
         return plan
 
     def rfft(self, a, out=None, axis=-1, **kwds):
@@ -871,10 +1358,9 @@ class GpyFFT(FFTI):
         return plan
 
     def irfft(self, a, out=None, n=None, axis=-1, **kwds):
-        (shape, dtype) = super(GpyFFT, self).irfft(a=a, out=out, axis=axis, 
-                                                        n=n, **kwds)
+        (shape, dtype, s) = super(GpyFFT, self).irfft(a=a, out=out, axis=axis, n=n, **kwds)
         out = self.allocate_output(out, shape, dtype) 
-        kwds = self.bake_kwds(a=a, out=out, axis=axis, **kwds)
+        kwds = self.bake_kwds(a=a, out=out, axis=axis, scale_by_size=s, **kwds)
         plan = GpyFFTPlan(**kwds)
         return plan
 
@@ -893,11 +1379,6 @@ class GpyFFT(FFTI):
             raise RuntimeError(msg)
         return plan
 
-    def idct(self, a, out=None, type=2, axis=-1, **kwds):
-        (shape, dtype, itype, logical_size) = super(GpyFFT, self).idct(a=a, out=out, type=type,
-                axis=axis, **kwds)
-        return self.dct(a=a, out=out, type=itype, axis=axis, scale_by_size=logical_size, **kwds)
-
     def dst(self, a, out=None, type=2, axis=-1, **kwds):
         (shape, dtype) = super(GpyFFT, self).dst(a=a, out=out, type=type, axis=axis, **kwds)
         out = self.allocate_output(out, shape, dtype) 
@@ -912,10 +1393,14 @@ class GpyFFT(FFTI):
             msg='Unimplemented sine transform type {}'.format(itype)
             raise RuntimeError(msg)
         return plan
+    
+    def idct(self, a, out=None, type=2, axis=-1, **kwds):
+        (shape, dtype, itype, s) = super(GpyFFT, self).idct(a=a, out=out, type=type,
+                axis=axis, **kwds)
+        return self.dct(a=a, out=out, type=itype, axis=axis, scale_by_size=s, **kwds)
 
     def idst(self, a, out=None, type=2, axis=-1, **kwds):
-        (shape, dtype, itype, logical_size) = super(GpyFFT, self).idst(a=a, out=out, type=type, 
+        (shape, dtype, itype, s) = super(GpyFFT, self).idst(a=a, out=out, type=type, 
                 axis=axis, **kwds)
-        kwds = self.bake_kwds(a=a, out=out, axis=axis, **kwds)
-        return self.dst(a=a, out=out, type=itype, axis=axis, scale_by_size=logical_size, **kwds)
+        return self.dst(a=a, out=out, type=itype, axis=axis, scale_by_size=s, **kwds)
 
diff --git a/hysop/numerics/fft/host_fft.py b/hysop/numerics/fft/host_fft.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ff82cae8ba592baff7cc289c9eb29b43bc5b154
--- /dev/null
+++ b/hysop/numerics/fft/host_fft.py
@@ -0,0 +1,126 @@
+"""
+OpenCl backend base interface for fast Fourier Transforms.
+
+:class:`~hysop.numerics.host_fft.HostFFTI`
+:class:`~hysop.numerics.host_fft.HostFFTPlanI`
+:class:`~hysop.numerics.host_fft.HostFFTQueue`
+"""
+
+import psutil
+import numpy as np
+
+from hysop import __FFTW_NUM_THREADS__, __FFTW_PLANNER_EFFORT__, __FFTW_PLANNER_TIMELIMIT__
+from hysop.tools.types import first_not_None, check_instance
+from hysop.backend.host.host_array_backend import HostArrayBackend
+from hysop.backend.host.host_array import HostArray
+from hysop.numerics.fft.fft import FFTQueueI, FFTPlanI, FFTI
+
+
+class HostFFTQueue(FFTQueueI):
+    """An host fft queue is just a tuple of callable objects."""
+    def __init__(self, name):
+        self._name = name
+        self._plans = ()
+
+    def __iadd__(self, *plans):
+        for plan in plans:
+            assert callable(plan)
+            self._plans += (plan,)
+        return self
+
+    def execute(self):
+        for plan in self._plans:
+            plan()
+
+
+class HostFFTPlanI(FFTPlanI):
+    """
+    Tag for FFT plans executing on host backend arrays.
+    """
+    pass
+
+
+class HostFFTI(FFTI):
+    """
+    Abstract base for FFT interfaces targetting Host backends.
+    """
+    def __init__(self, backend=None, allocator=None, **kwds):
+        from hysop.backend.host.host_array_backend import HostArrayBackend
+        if (backend is None):
+            backend = HostArrayBackend.get_or_create(allocator=allocator)
+        if (allocator is not None):
+            mdg='Host allocator does not match the one of the backend.'
+            assert (backend.allocator is allocator)
+        check_instance(backend, HostArrayBackend)
+        super(HostFFTI, self).__init__(backend=backend, **kwds)
+
+    @classmethod
+    def default_interface(cls, threads=None, 
+                       backend=None, allocator=None,
+                       planner_effort=None, 
+                       planning_timelimit=None,
+                       destroy_input=False,
+                       warn_on_allocation=True,
+                       warn_on_misalignment=True,
+                       error_on_allocation=False,
+                       **kwds):
+        """
+        Get the default host FFT interface which is a multithreaded FFTW interface with 
+        ESTIMATE planning effort.
+        """
+        threads            = first_not_None(threads,            __FFTW_NUM_THREADS__)
+        planner_effort     = first_not_None(planner_effort,     __FFTW_PLANNER_EFFORT__)
+        planning_timelimit = first_not_None(planning_timelimit, __FFTW_PLANNER_TIMELIMIT__)
+        from hysop.numerics.fft.fftw_fft import FftwFFT
+        return FftwFFT(threads=threads,  
+                       planner_effort=planner_effort, 
+                       planning_timelimit=planning_timelimit,
+                       backend=backend, allocator=allocator,
+                       destroy_input=destroy_input,
+                       warn_on_allocation=warn_on_allocation,
+                       warn_on_misalignment=warn_on_misalignment,
+                       error_on_allocation=error_on_allocation,
+                       **kwds)
+    
+    def new_queue(self, tg, name):
+        return HostFFTQueue(name=name)
+                       
+    def plan_copy(self, tg, src, dst):
+        src = self.ensure_callable(src)
+        dst = self.ensure_callable(dst)
+        def exec_copy(src=src, dst=dst):
+            dst()[...] = src()
+        return exec_copy
+    
+    def plan_accumulate(self, tg, src, dst):
+        src = self.ensure_callable(src)
+        dst = self.ensure_callable(dst)
+        def exec_copy(src=src, dst=dst):
+            dst()[...] += src()
+        return exec_copy
+
+    def plan_transpose(self, tg, src, dst, axes):
+        src = self.ensure_callable(src)
+        dst = self.ensure_callable(dst)
+        def exec_transpose(src=src, dst=dst, axes=axes):
+            dst()[...] = np.transpose(a=src(), axes=axes)
+        return exec_transpose
+    
+    def plan_fill_zeros(self, tg, a, slices):
+        assert slices
+        a = self.ensure_callable(a)
+        def exec_fill_zeros(a=a, slices=slices):
+            buf = a()
+            for slc in slices:
+                buf[slc] = 0
+        return exec_fill_zeros
+
+
+    @classmethod
+    def ensure_callable(cls, get_buffer):
+        if callable(get_buffer):
+            return get_buffer
+        else:
+            def get_buf(buf=get_buffer):
+                return buf
+            return get_buf
diff --git a/hysop/numerics/fft/numpy_fft.py b/hysop/numerics/fft/numpy_fft.py
index e49713d66cb72804c1ca17f27abf48ee3b1f6132..afb0261c6d23d1cb5d3b78b0473c2767f90f052f 100644
--- a/hysop/numerics/fft/numpy_fft.py
+++ b/hysop/numerics/fft/numpy_fft.py
@@ -1,5 +1,5 @@
 """
-FFT iterface for fast Fourier Transforms using numpy fft backend.
+FFT iterface for fast Fourier Transforms using C fftpack fork (using numpy).
 :class:`~hysop.numerics.NumpyFFT`
 :class:`~hysop.numerics.NumpyFFTPlan`
 """
@@ -8,9 +8,10 @@ import numpy as np
 from numpy import fft as _FFT
 
 from hysop.tools.types import first_not_None
-from hysop.numerics.fft.fft import FFTPlanI, FFTI, \
+from hysop.numerics.fft.host_fft import HostFFTPlanI, HostFFTI, HostArray
+from hysop.numerics.fft.fft import \
             complex_to_float_dtype, float_to_complex_dtype, \
-            mk_view, mk_shape, HostArray
+            mk_view, mk_shape
 
 def dct(a, out=None, type=2, axis=-1):
     ndim  = a.ndim
@@ -95,8 +96,8 @@ def dst(a, out=None, type=2, axis=-1):
         # O(sqrt(log(N))) error, O(2N) complexity, O(4*N) memory
         slc0  = mk_view(ndim, axis, None, None, -1)
         slc1  = mk_view(ndim, axis, 1,    -1,   None)
-        s0    = mk_shape(shape, axis, 2*N+2)
-        s1    = (1,)*ndim
+        s0 = mk_shape(shape, axis, 2*N+2)
+        s1 = mk_shape(shape, axis, 1)
         X = np.empty(shape=s0, dtype=a.dtype)
         Z = np.zeros(shape=s1, dtype=a.dtype)
         np.concatenate((Z, -a, Z, a[slc0]), axis=axis, out=X)
@@ -175,7 +176,7 @@ def idst(a, out=None, type=2, axis=-1, **kwds):
     return dst(a=a, out=out, type=itype, axis=axis, **kwds)
 
 
-class NumpyFFTPlan(FFTPlanI):
+class NumpyFFTPlan(HostFFTPlanI):
     """
     Wrap a numpy fft call (numpy.fft does not offer real planning capabilities). 
     """
@@ -220,7 +221,7 @@ class NumpyFFTPlan(FFTPlanI):
             out[...] *= scaling
 
 
-class NumpyFFT(FFTI):
+class NumpyFFT(HostFFTI):
     """
     Interface to compute local to process FFT-like transforms using the numpy fft backend.
 
@@ -233,8 +234,10 @@ class NumpyFFT(FFTI):
     The only advantage is that planning won't destroy original inputs.
     """
 
-    def __init__(self, backend=None, **kwds):
-        super(NumpyFFT, self).__init__(backend=backend, **kwds)
+    def __init__(self, backend=None, allocator=None, 
+                        warn_on_allocation=True, **kwds):
+        super(NumpyFFT, self).__init__(backend=backend, allocator=allocator, 
+                warn_on_allocation=warn_on_allocation, **kwds)
         self.supported_ftypes = (np.float32, np.float64,)
         self.supported_ctypes = (np.complex64, np.complex128,)
     
@@ -246,7 +249,7 @@ class NumpyFFT(FFTI):
         return plan
 
     def ifft(self, a, out=None, axis=-1, **kwds):
-        (shape, dtype) = super(NumpyFFT, self).ifft(a=a, out=out, axis=axis, **kwds)
+        (shape, dtype, s) = super(NumpyFFT, self).ifft(a=a, out=out, axis=axis, **kwds)
         out = self.allocate_output(out, shape, dtype) 
         plan = NumpyFFTPlan(fn=_FFT.ifft, a=a, out=out, axis=axis, **kwds)
         return plan
@@ -259,7 +262,7 @@ class NumpyFFT(FFTI):
         return plan
 
     def irfft(self, a, out=None, n=None, axis=-1, **kwds):
-        (shape, dtype) = super(NumpyFFT, self).irfft(a=a, out=out, n=n, axis=axis, **kwds)
+        (shape, dtype, s) = super(NumpyFFT, self).irfft(a=a, out=out, n=n, axis=axis, **kwds)
         out = self.allocate_output(out, shape, dtype) 
         plan = NumpyFFTPlan(fn=_FFT.irfft, a=a, out=out, axis=axis, 
                             n=shape[axis], **kwds)
diff --git a/hysop/numerics/fft/opencl_fft.py b/hysop/numerics/fft/opencl_fft.py
new file mode 100644
index 0000000000000000000000000000000000000000..b67149686b12dc42c9adf66c58fbb0e981992641
--- /dev/null
+++ b/hysop/numerics/fft/opencl_fft.py
@@ -0,0 +1,150 @@
+"""
+OpenCl backend base interface for fast Fourier Transforms.
+
+:class:`~hysop.numerics.opencl_fft.OpenClFFTI`
+:class:`~hysop.numerics.opencl_fft.OpenClFFTPlanI`
+:class:`~hysop.numerics.opencl_fft.OpenClFFTQueue`
+"""
+
+from abc import abstractmethod
+from hysop.tools.types import first_not_None, check_instance
+from hysop.numerics.fft.fft import FFTQueueI, FFTPlanI, FFTI
+from hysop.symbolic.relational import Assignment
+from hysop.backend.device.opencl.opencl_array_backend import OpenClArrayBackend
+from hysop.backend.device.opencl.opencl_array import OpenClArray
+from hysop.backend.device.opencl.opencl_elementwise import OpenClElementwiseKernelGenerator
+from hysop.backend.device.opencl.opencl_kernel_launcher import OpenClKernelListLauncher, \
+                                                               OpenClKernelLauncherI
+from hysop.backend.device.opencl.opencl_copy_kernel_launchers import \
+                                            OpenClCopyBufferRectLauncher, OpenClFillKernelLauncher
+from hysop.backend.device.opencl.opencl_kernel_launcher import OpenClKernelListLauncher
+from hysop.backend.device.opencl.autotunable_kernels.transpose import \
+                                                        OpenClAutotunableTransposeKernel
+
+
+class OpenClFFTQueue(FFTQueueI):
+    """An opencl fft queue is just like a standard opencl queue."""
+    def __init__(self, queue, name='fft_planner'):
+        self._launcher = OpenClKernelListLauncher(name=name)
+        self._queue = queue
+
+    def __iadd__(self, kernel):
+        self._launcher += kernel
+        return self
+
+    def execute(self):
+        return self._launcher(queue=self._queue)
+
+
+class OpenClFFTPlanI(FFTPlanI, OpenClKernelLauncherI):
+    """
+    Tag for FFT plans executing on OpenCL backend arrays.
+    """
+    def __init__(self, **kwds):
+        super(OpenClFFTPlanI, self).__init__(**kwds)
+        self._name = 'fft_plan'
+
+    @abstractmethod
+    def execute(self, **kwds):
+        pass
+
+    def __call__(self, **kwds):
+        return self.execute(**kwds)
+
+    def global_size_configured(self):
+        return True
+
+
+class OpenClFFTI(FFTI):
+    """
+    Abstract base for FFT interfaces targetting OpenCL backends.
+    """
+    def __init__(self, cl_env, backend=None, allocator=None, **kwds):
+        from hysop.backend.device.opencl.opencl_array_backend import OpenClArrayBackend
+        from hysop.backend.device.opencl.opencl_env import OpenClEnvironment
+        if (backend is None):
+            backend = OpenClArrayBackend.get_or_create(cl_env=cl_env, 
+                            queue=cl_env.default_queue, allocator=allocator)
+        else:
+            msg='OpenCl environment does not match the one of the backend.'
+            assert (backend.cl_env is cl_env), msg
+        if (allocator is not None):
+            msg='OpenCl allocator does not match the one of the backend.'
+            assert (backend.allocator is allocator), msg
+        check_instance(cl_env,  OpenClEnvironment)
+        check_instance(backend, OpenClArrayBackend)
+        super(OpenClFFTI, self).__init__(backend=backend, **kwds)
+        self.cl_env = cl_env
+        self.kernel_generator = OpenClElementwiseKernelGenerator(cl_env=cl_env)
+
+    @classmethod
+    def default_interface(cls, cl_env,
+                       backend=None, allocator=None,
+                       warn_on_allocation=False,
+                       error_on_allocation=True,
+                       warn_on_unaligned_output_offset=True,
+                       **kwds):
+        """Get the default OpenCl FFT interface which is a GpyFFT interface."""
+        from hysop.numerics.fft.gpyfft_fft import GpyFFT
+        return GpyFFT(cl_env=cl_env,
+                      backend=backend, allocator=allocator,
+                      warn_on_allocation=warn_on_allocation,
+                      error_on_allocation=error_on_allocation,
+                      warn_on_unaligned_output_offset=warn_on_unaligned_output_offset,
+                      **kwds)
+
+    def new_queue(self, tg, name):
+        return OpenClFFTQueue(queue=tg.backend.cl_env.default_queue, name=name)
+                       
+    def plan_copy(self, tg, src, dst):
+        src = self.ensure_buffer(src)
+        dst = self.ensure_buffer(dst)
+        launcher = OpenClCopyBufferRectLauncher.from_slices('copy',
+                            src=src, dst=dst)
+        return launcher
+    
+    def plan_accumulate(self, tg, src, dst):
+        src = self.ensure_buffer(src)
+        dst = self.ensure_buffer(dst)
+        src, dst = self.kernel_generator.arrays_to_symbols(src, dst)
+        expr = Assignment(dst, src+dst)
+        launcher, _ = self.kernel_generator.elementwise_kernel('accumulate', expr)
+        return launcher
+
+    def plan_transpose(self, tg, src, dst, axes):
+        src = self.ensure_buffer(src)
+        dst = self.ensure_buffer(dst)
+        backend_kwds = {
+                'cl_env':           tg.backend.cl_env,
+                'typegen':          tg.op.typegen,
+                'autotuner_config': tg.op.autotuner_config,
+                'build_opts':       tg.op.build_options()
+        }
+        kernel = OpenClAutotunableTransposeKernel(**backend_kwds)
+        transpose, _ = kernel.autotune(axes=axes, 
+                                       hardcode_arrays=True,
+                                       is_inplace=False,
+                                       input_buffer=src,
+                                       output_buffer=dst)
+        return transpose.build_launcher(in_base=src.base_data, out_base=dst.base_data)
+    
+    def plan_fill_zeros(self, tg, a, slices):
+        if not slices:
+            return
+        a = self.ensure_buffer(a)
+        launcher = OpenClKernelListLauncher(name='fill_zeros')
+        for slc in slices:
+            lnc = OpenClFillKernelLauncher.from_slices(varname='buffer', 
+                    backend=tg.backend,
+                    dst=a[slc], 
+                    fill_value=0)
+            launcher += lnc
+        return launcher
+    
+    @classmethod
+    def ensure_buffer(cls, get_buffer):
+        if callable(get_buffer):
+            buf = get_buffer()
+        else:
+            buf = get_buffer
+        return buf
diff --git a/hysop/numerics/fft/scipy_fft.py b/hysop/numerics/fft/scipy_fft.py
index cf7c76042240e34f924f80f0b442836a1333114b..2847f113aabc6c57d7181b0d11206650563cc7f0 100644
--- a/hysop/numerics/fft/scipy_fft.py
+++ b/hysop/numerics/fft/scipy_fft.py
@@ -1,5 +1,5 @@
 """
-FFT iterface for fast Fourier Transforms using scipy fftpack backend.
+FFT iterface for fast Fourier Transforms using scipy fftpack backend (using scipy).
 :class:`~hysop.numerics.ScipyFFT`
 :class:`~hysop.numerics.ScipyFFTPlan`
 """
@@ -9,12 +9,13 @@ 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, \
+from hysop.numerics.fft.host_fft import HostFFTPlanI, HostFFTI, HostArray
+from hysop.numerics.fft.fft import \
         complex_to_float_dtype, float_to_complex_dtype, \
-        mk_shape, mk_view, HostArray
+        mk_shape, mk_view
 
 
-class ScipyFFTPlan(FFTPlanI):
+class ScipyFFTPlan(HostFFTPlanI):
     """
     Wrap a scipy fftpack call (scipy.fftpack does not offer real planning capabilities). 
     """
@@ -59,7 +60,10 @@ class ScipyFFTPlan(FFTPlanI):
         mkv = lambda *args, **kwds: mk_view(x.ndim, axis, *args, **kwds)
 
         if fn is _FFT.irfft:
-            x = x.view(dtype=complex_to_float_dtype(x.dtype))
+            try:
+                x = x.view(dtype=complex_to_float_dtype(x.dtype))
+            except ValueError:
+                x = x.copy().view(dtype=complex_to_float_dtype(x.dtype))
             is_even = (fn_kwds['n']%2==0)
             rshape = list(x.shape)
             rshape[axis] -= (1 + is_even)
@@ -87,23 +91,33 @@ class ScipyFFTPlan(FFTPlanI):
             res[slc2] = res[slc3]
             res = res[slc4]
         elif fn in (_FFT.dst, _FFT.idst) and fn_kwds['type']==1:
-            assert axis in (x.ndim-1, -1)
             N = x.shape[axis]
-            X = np.hstack((0, -x, 0, +x[::-1])).astype(x.dtype)
-            res = _FFT.rfft(X, axis=axis)[2::2]
+            shape = x.shape
+            ndim  = x.ndim
+            slc0 = mk_view(ndim, axis, None, None, -1)
+            slc1 = mk_view(ndim, axis, 2,    None,  2)
+            s0 = mk_shape(shape, axis, 2*N+2)
+            s1 = mk_shape(shape, axis, 1)
+            X = np.empty(shape=s0, dtype=x.dtype)
+            Z = np.zeros(shape=s1, dtype=x.dtype)
+            np.concatenate((Z, -x, Z, x[slc0]), axis=axis, out=X)
+            res = _FFT.rfft(X, axis=axis)[slc1]
         else:
             res = fn(**fn_kwds)
 
         if fn is _FFT.rfft:
             assert axis in (x.ndim-1, -1)
             is_even = (x.shape[axis] % 2 == 0) 
-            if (out is None):
-                rshape = list(res.shape)
-                rshape[axis] += 1 + is_even
+            rshape = list(res.shape)
+            rshape[axis] += 1 + is_even
+            if (out is None) or (not out.flags.c_contiguous):
+                real_output=out
                 out = np.empty(dtype=x.dtype, shape=rshape)
             else:
                 rtype = complex_to_float_dtype(out.dtype)
+                real_output=None
                 out = out.view(dtype=rtype)
+            assert np.array_equal(out.shape, rshape)
             ctype = float_to_complex_dtype(out.dtype)
             out[mkv(1,out.shape[axis]-is_even)] = res
             out[mkv(0)] = out[mkv(1)]
@@ -111,6 +125,9 @@ class ScipyFFTPlan(FFTPlanI):
             if is_even:
                 out[mkv(-1)] = 0.0
             out = out.view(dtype=ctype)
+            if (real_output is not None):
+                real_output[...] = out
+                out = real_output
         elif (out is not None):
             out[...] = res
         else:
@@ -123,7 +140,7 @@ class ScipyFFTPlan(FFTPlanI):
         return out
 
 
-class ScipyFFT(FFTI):
+class ScipyFFT(HostFFTI):
     """
     Interface to compute local to process FFT-like transforms using the scipy fftpack backend.
 
@@ -136,8 +153,10 @@ class ScipyFFT(FFTI):
     Planning won't destroy original inputs.
     """
 
-    def __init__(self, **kwds):
-        super(ScipyFFT, self).__init__(**kwds)
+    def __init__(self, backend=None, allocator=None,
+            warn_on_allocation=True, **kwds):
+        super(ScipyFFT, self).__init__(backend=backend, allocator=allocator, 
+                warn_on_allocation=warn_on_allocation, **kwds)
         self.supported_ftypes = (np.float32, np.float64,)
         self.supported_ctypes = (np.complex64, np.complex128,)
     
@@ -148,7 +167,7 @@ class ScipyFFT(FFTI):
         return plan
 
     def ifft(self, a, out=None, axis=-1, **kwds):
-        (shape, dtype) = super(ScipyFFT, self).ifft(a=a, out=out, axis=axis, **kwds)
+        (shape, dtype, s) = super(ScipyFFT, self).ifft(a=a, out=out, axis=axis, **kwds)
         out = self.allocate_output(out, shape, dtype) 
         plan = ScipyFFTPlan(fn=_FFT.ifft, x=a, out=out, axis=axis, **kwds)
         return plan
@@ -161,7 +180,7 @@ class ScipyFFT(FFTI):
         return plan
 
     def irfft(self, a, out=None, n=None, axis=-1, **kwds):
-        (shape, dtype) = super(ScipyFFT, self).irfft(a=a, out=out, n=n, axis=axis, **kwds)
+        (shape, dtype, s) = super(ScipyFFT, self).irfft(a=a, out=out, n=n, axis=axis, **kwds)
         out = self.allocate_output(out, shape, dtype) 
         plan = ScipyFFTPlan(fn=_FFT.irfft, x=a, out=out, axis=axis, 
                             n=shape[axis], **kwds)
diff --git a/hysop/numerics/splitting/test/test_strang.py b/hysop/numerics/splitting/test/test_strang.py
index 77004baa9cf3365ba7e57ba6073baf5820a81200..fb86d81c959aaedfb75a9050fbbeb90ba42c93fd 100644
--- a/hysop/numerics/splitting/test/test_strang.py
+++ b/hysop/numerics/splitting/test/test_strang.py
@@ -6,7 +6,7 @@ from hysop.numerics.splitting.strang import StrangSplitting
 from hysop.domain.box         import Box
 from hysop.fields.continuous_field  import Field
 from hysop.topology.cartesian_topology import CartesianTopology
-from hysop.tools.parameters   import Discretization
+from hysop.tools.parameters   import CartesianDiscretization
 from hysop.simulation import Simulation
 
 from hysop.methods import *
@@ -55,9 +55,9 @@ class TestStrang(object):
         ## Domain and continuous fields
         box, velo, vorti, scalars = self.make_fields(dim,2)
 
-        ## Discretizations and topologies
+        ## CartesianDiscretizations and topologies
         resolution     = (n,)*dim
-        dnd            = Discretization(resolution=resolution, ghosts=None)
+        dnd            = CartesianDiscretization(resolution=resolution, ghosts=None)
         topo           = CartesianTopology(box,dnd,dim)
 
         ## Operators
diff --git a/hysop/numerics/stencil/stencil.py b/hysop/numerics/stencil/stencil.py
index 422a70883c1a7bed496e7c9387c1024b9b7dd460..d88df37cadddfd9a3ebfc8a4926e8226e8f16635 100644
--- a/hysop/numerics/stencil/stencil.py
+++ b/hysop/numerics/stencil/stencil.py
@@ -164,10 +164,12 @@ class Stencil(object):
             raccess = [slice(None)]*dim
             laccess[d] = slice(0,1)
             raccess[d] = slice(-1,None)
-            ldel = mask[tuple(laccess)].all()
-            rdel = mask[tuple(raccess)].all()
+            laccess = tuple(laccess)
+            raccess = tuple(raccess)
+            ldel = mask[laccess].all()
+            rdel = mask[raccess].all()
             if ldel:
-                keep_mask[tuple(laccess)] = False
+                keep_mask[laccess] = False
                 if dim==1:
                     self.origin-=1
                 else:
@@ -175,7 +177,7 @@ class Stencil(object):
                 shape[d]-=1
             if rdel:
                 shape[d]-=1
-                keep_mask[tuple(raccess)] = False
+                keep_mask[raccess] = False
         coeffs = coeffs[keep_mask].reshape(shape)
         return coeffs
 
diff --git a/hysop/numerics/stencil/stencil_generator.py b/hysop/numerics/stencil/stencil_generator.py
index 93ca97707278cf994f70c3a4875367387648c1e3..2e38546b61e6f1125ef6b1f35c7c5f3849eca855 100644
--- a/hysop/numerics/stencil/stencil_generator.py
+++ b/hysop/numerics/stencil/stencil_generator.py
@@ -144,12 +144,16 @@ class StencilGeneratorConfiguration(object):
             for d in xrange(dim):
                 access = [slice(origin[dd],origin[dd]+1) for dd in xrange(dim)]
                 access[d] = slice(None)
+                access = tuple(access)
+                mask[access] = True
                 mask[tuple(access)] = True
         elif mask==StencilGenerator.DIAG:
             mask = np.ones(shape,dtype=bool)
             for d in xrange(dim):
                 access = [slice(origin[dd],origin[dd]+1) for dd in xrange(dim)]
                 access[d] = slice(None)
+                access = tuple(access)
+                mask[access] = False
                 mask[tuple(access)] = False
             mask[origin] = True
         elif mask==StencilGenerator.DENSE:
diff --git a/hysop/numerics/tests/test_fft.py b/hysop/numerics/tests/test_fft.py
index b3ce1078de7927c26a2bfbfb8d3e0acb24b77c56..4b42e4e001d74ed1e7f9c4d6631e9ea3ea768f9a 100644
--- a/hysop/numerics/tests/test_fft.py
+++ b/hysop/numerics/tests/test_fft.py
@@ -8,14 +8,14 @@ import numpy as np
 import itertools as it
 
 from hysop.deps import it, sm, random
-from hysop.constants import Implementation
+from hysop.constants import Implementation, HYSOP_REAL
 from hysop.testsenv import __ENABLE_LONG_TESTS__, __HAS_OPENCL_BACKEND__
 from hysop.testsenv import opencl_failed, iter_clenv
 from hysop.tools.contexts import printoptions
 from hysop.tools.numerics import float_to_complex_dtype
 from hysop.tools.types import check_instance, first_not_None
 
-from hysop.numerics.fft.fft  import mk_shape
+from hysop.numerics.fft.fft  import mk_shape, HysopFFTDataLayoutError
 from hysop.numerics.fft.numpy_fft  import NumpyFFT
 from hysop.numerics.fft.scipy_fft  import ScipyFFT
 from hysop.numerics.fft.fftw_fft   import FftwFFT
@@ -34,9 +34,12 @@ class TestFFT(object):
         Implementation.OPENCL: {}
     }
     
+    print
+    print ':: STARTING FFT BACKEND TESTS ::'
     for (i,cl_env) in enumerate(iter_clenv()):
         print '> Registering opencl backend {} as:\n{}'.format(
                 i, cl_env)
+        print
         name = 'clfft{}'.format(i)
         implementations[Implementation.OPENCL][name] = \
             GpyFFT(cl_env=cl_env,
@@ -48,26 +51,64 @@ class TestFFT(object):
     msg_input_modified  = 'Input array has been modified for implementation {}.'
     msg_output_modified = 'Output array results are not consistent for implementation {}.'
 
-    report_eps = 25
-    fail_eps = 1000
+    report_eps = 10
+    fail_eps   = 100
 
     def _test_1d(self, dtype, failures):
         print
         print '::Testing 1D transform, precision {}::'.format(dtype.__name__)
         eps = np.finfo(dtype).eps
         ctype = float_to_complex_dtype(dtype)
-        
-        if False:
-            print '\n FORWARD C2C: complex to complex forward transform'
-            for (shape, cshape, rshape, N, Nc, Nr) in self.iter_shapes():
-                print '   FFT shape={:9s} '.format(str(shape)+':'),
-                Href = np.random.rand(2*N).astype(dtype).view(dtype=ctype).reshape(shape)
-                results = {}
-                for (kind, implementations) in self.implementations.iteritems():
-                    for (name, impl) in implementations.iteritems():
-                        if dtype not in impl.supported_ftypes:
-                            continue
-                        Bin = impl.backend.empty(shape=shape, dtype=ctype)
+    
+        def check_distances(results, eps, report_eps, tag, failures):
+            if len(results.keys())==0:
+                print 'no support'
+                return
+            elif len(results.keys())==1:
+                impl = results.keys()[0]
+                print 'cannot compare'
+                return
+            ss=()
+            for (r0,r1) in it.combinations(results.keys(), 2):
+                E0 = results[r0]
+                E1 = results[r1]
+                if isinstance(E0, HysopFFTDataLayoutError) or isinstance(E1, HysopFFTDataLayoutError):
+                    s='|{}-{}|=N.A.'.format(r0,r1)
+                    failed=False
+                elif not (E0.shape == E1.shape):
+                        print
+                        msg='Output shapes do not match.'
+                        raise RuntimeError(msg)
+                else:
+                    E = results[r1] - results[r0]
+                    Einf = np.max(np.abs(E))
+                    if np.isfinite(Einf):
+                        Eeps = int(np.round(Einf/eps))
+                        s='|{}-{}|={}eps'.format(r0,r1,Eeps)
+                        failed = (Eeps > report_eps)
+                    else:
+                        Eeps = Einf
+                        s='|{}-{}|={}'.format(r0,r1,str(Eeps).upper())
+                        failed = True
+                ss += (s,)
+                if failed:
+                    shape=results[r0].shape
+                    failures.setdefault(tag, []).append((r0, r1, shape, Einf, Eeps))
+            print ', '.join(ss)
+
+
+        print '\n FORWARD C2C: complex to complex forward transform'
+        for (shape, cshape, rshape, N, Nc, Nr,
+                ghosts, mk_buffer) in self.iter_shapes():
+            print '   FFT shape={:12s} ghosts={:12s} '.format(shape, str(ghosts)+':'),
+            Href = np.random.rand(2*N).astype(dtype).view(dtype=ctype).reshape(shape)
+            results = {}
+            for (kind, implementations) in self.implementations.iteritems():
+                for (name, impl) in implementations.iteritems():
+                    if dtype not in impl.supported_ftypes:
+                        continue
+                    try:
+                        Bin = mk_buffer(backend=impl.backend, shape=shape, dtype=ctype)
                         plan = impl.fft(a=Bin).setup()
                         Bout = plan.output_array
                         assert Bout.shape == shape, self.msg_shape.format(shape, Bout.shape, 
@@ -83,20 +124,25 @@ class TestFFT(object):
                         evt = plan.execute()
                         assert plan.output_array is Bout
                         H2 = Bout.get()
-                        assert np.array_equal(H1, H2), self.msg_output_modified.format(name)
-                        results[name] = H1 / N # forward normalization
-                self.check_distances(results, eps, self.report_eps, 'forward C2C', failures)
-            
-            print '\n BACKWARD C2C: complex to complex backward transform'
-            for (shape, cshape, rshape, N, Nc, Nr) in self.iter_shapes():
-                print '   IFFT shape={:9s} '.format(str(shape)+':'),
-                Href = np.random.rand(2*N).astype(dtype).view(dtype=ctype).reshape(shape)
-                results = {}
-                for (kind, implementations) in self.implementations.iteritems():
-                    for (name, impl) in implementations.iteritems():
-                        if dtype not in impl.supported_ftypes:
-                            continue
-                        Bin = impl.backend.empty(shape=shape, dtype=ctype)
+                        assert np.allclose(H1, H2, atol=eps), \
+                                self.msg_output_modified.format(name)
+                        results[name] = H1 / shape[-1] # forward normalization
+                    except HysopFFTDataLayoutError as e:
+                        results[name] = e
+            check_distances(results, eps, self.report_eps, 'forward C2C', failures)
+        
+        print '\n BACKWARD C2C: complex to complex backward transform'
+        for (shape, cshape, rshape, N, Nc, Nr, 
+                ghosts, mk_buffer) in self.iter_shapes():
+            print '   IFFT shape={:12s} ghosts={:12s} '.format(shape, str(ghosts)+':'),
+            Href = np.random.rand(2*N).astype(dtype).view(dtype=ctype).reshape(shape)
+            results = {}
+            for (kind, implementations) in self.implementations.iteritems():
+                for (name, impl) in implementations.iteritems():
+                    if dtype not in impl.supported_ftypes:
+                        continue
+                    try:
+                        Bin = mk_buffer(backend=impl.backend, shape=shape, dtype=ctype)
                         plan = impl.ifft(a=Bin).setup()
                         Bout = plan.output_array
                         assert Bout.shape == shape, self.msg_shape.format(shape, Bout.shape, 
@@ -112,118 +158,95 @@ class TestFFT(object):
                         evt = plan.execute()
                         assert plan.output_array is Bout
                         H2 = Bout.get()
-                        assert np.array_equal(H1, H2), self.msg_output_modified.format(name)
-                        results[name] = H1 / N # forward normalization
-                self.check_distances(results, eps, self.report_eps, 'backward C2C', failures)
+                        assert np.allclose(H1, H2, atol=eps), \
+                                self.msg_output_modified.format(name)
+                        results[name] = H1 / shape[-1] # forward normalization
+                    except HysopFFTDataLayoutError as e:
+                        results[name] = e
+            check_distances(results, eps, self.report_eps, 'backward C2C', failures)
             
         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)
+        for (shape, cshape, rshape, N, Nc, Nr, 
+                ghosts, mk_buffer) in self.iter_shapes():
+            print '   RFFT shape={:12s} ghosts={:12s} '.format(shape, str(ghosts)+':'),
+            Href = np.random.rand(*shape).astype(dtype).reshape(shape)
             results = {}
             for (kind, implementations) in self.implementations.iteritems():
                 for (name, impl) in implementations.iteritems():
                     if dtype not in impl.supported_ftypes:
                         continue
-                    Bin = impl.backend.empty(shape=shape, dtype=dtype)
-                    plan = impl.rfft(a=Bin).setup()
-                    Bout = plan.output_array
-                    assert Bout.shape == cshape, self.msg_shape.format(cshape, Bout.shape, 
-                            name)
-                    assert Bout.dtype == ctype, self.msg_dtype.format(ctype, Bout.dtype, 
-                            name)
-                    Bin[...] = Href
-                    plan.execute()
-                    H0 = Bin.get()
-                    H1 = Bout.get()
-                    assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
-                    Bout[...] = 0
-                    evt = plan.execute()
-                    assert plan.output_array is Bout
-                    H2 = Bout.get()
-                    assert np.array_equal(H1, H2), self.msg_output_modified.format(name)
-                    results[name] = H1 / N # forward normalization
-            self.check_distances(results, eps, self.report_eps, 'R2C', failures)
+                    try:
+                        Bin = mk_buffer(backend=impl.backend, shape=shape, dtype=dtype)
+                        plan = impl.rfft(a=Bin).setup()
+                        Bout = plan.output_array
+                        assert Bout.shape == cshape, self.msg_shape.format(cshape, Bout.shape, 
+                                name)
+                        assert Bout.dtype == ctype, self.msg_dtype.format(ctype, Bout.dtype, 
+                                name)
+                        Bin[...] = Href
+                        plan.execute()
+                        H0 = Bin.get()
+                        H1 = Bout.get()
+                        assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
+                        Bout[...] = 0
+                        evt = plan.execute()
+                        assert plan.output_array is Bout
+                        H2 = Bout.get()
+                        assert np.allclose(H1, H2, atol=eps), \
+                                self.msg_output_modified.format(name)
+                        results[name] = H1 / shape[-1] # forward normalization
+                    except HysopFFTDataLayoutError as e:
+                        results[name] = e
+            check_distances(results, eps, self.report_eps, 'R2C', failures)
         
         print '\n BACKWARD C2R: real to hermitian complex transform'
-        for (shape, cshape, rshape, N, Nc, Nr) in self.iter_shapes():
-            print '   IRFFT shape={:9s} '.format(str(shape)+':'),
+        for (shape, cshape, rshape, N, Nc, Nr, 
+                ghosts, mk_buffer) in self.iter_shapes():
+            print '   IRFFT shape={:12s} ghosts={:12s} '.format(shape, str(ghosts)+':'),
             Href = np.random.rand(2*Nc).astype(dtype).view(dtype=ctype).reshape(cshape)
             results = {}
             for (kind, implementations) in self.implementations.iteritems():
                 for (name, impl) in implementations.iteritems():
                     if dtype not in impl.supported_ftypes:
                         continue
-                    Bin = impl.backend.empty(shape=cshape, dtype=ctype)
-                    plan = impl.irfft(a=Bin).setup()
-                    Bout = plan.output_array
-                    assert Bout.shape == rshape, self.msg_shape.format(rshape, Bout.shape, 
-                            name)
-                    assert Bout.dtype == dtype, self.msg_dtype.format(dtype, Bout.dtype, 
-                            name)
-                    Bin[...] = Href
-                    plan.execute()
-                    H0 = Bin.get()
-                    H1 = Bout.get()
-                    assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
-                    Bout[...] = 0
-                    evt = plan.execute()
-                    assert plan.output_array is Bout
-                    H2 = Bout.get()
-                    assert np.array_equal(H1, H2), self.msg_output_modified.format(name)
-                    results[name] = H1
-            self.check_distances(results, eps, self.report_eps, 'normal C2R', failures)
-        
+                    try:
+                        Bin = mk_buffer(backend=impl.backend, shape=cshape, dtype=ctype)
+                        plan = impl.irfft(a=Bin).setup()
+                        Bout = plan.output_array
+                        assert Bout.shape == rshape, self.msg_shape.format(rshape, Bout.shape, 
+                                name)
+                        assert Bout.dtype == dtype, self.msg_dtype.format(dtype, Bout.dtype, 
+                                name)
+                        Bin[...] = Href
+                        plan.execute()
+                        H0 = Bin.get()
+                        H1 = Bout.get()
+                        assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
+                        Bout[...] = 0
+                        evt = plan.execute()
+                        assert plan.output_array is Bout
+                        H2 = Bout.get()
+                        assert np.allclose(H1, H2, atol=eps), \
+                                self.msg_output_modified.format(name)
+                        results[name] = H1
+                    except HysopFFTDataLayoutError as e:
+                        results[name] = e
+            check_distances(results, eps, self.report_eps, 'normal C2R', failures)
+
         print ('\n BACKWARD FORCED C2R: real to hermitian complex transform with specified '
                 +'shape')
-        for (shape, cshape, rshape, N, Nc, Nr) in self.iter_shapes():
-            print '   IRFFT shape={:9s} '.format(str(shape)+':'),
+        for (shape, cshape, rshape, N, Nc, Nr, 
+                ghosts, mk_buffer) in self.iter_shapes():
+            print '   IRFFT shape={:12s} ghosts={:12s} '.format(shape, str(ghosts)+':'),
             Href = np.random.rand(2*Nc).astype(dtype).view(dtype=ctype).reshape(cshape)
             results = {}
             for (kind, implementations) in self.implementations.iteritems():
                 for (name, impl) in implementations.iteritems():
                     if dtype not in impl.supported_ftypes:
                         continue
-                    Bin = impl.backend.empty(shape=cshape, dtype=ctype)
-                    plan = impl.irfft(a=Bin, n=shape[-1]).setup()
-                    Bout = plan.output_array
-                    assert Bout.shape == shape, self.msg_shape.format(shape, Bout.shape, 
-                            name)
-                    assert Bout.dtype == dtype, self.msg_dtype.format(dtype, Bout.dtype, 
-                            name)
-                    Bin[...] = Href
-                    plan.execute()
-                    H0 = Bin.get()
-                    H1 = Bout.get()
-                    assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
-                    Bout[...] = 0
-                    evt = plan.execute()
-                    assert plan.output_array is Bout
-                    H2 = Bout.get()
-                    assert np.array_equal(H1, H2), self.msg_output_modified.format(name)
-                    results[name] = H1
-            self.check_distances(results, eps, self.report_eps, 'forced C2R', failures)
-
-        types = ['I  ','II ','III','IV ']
-        for (itype,stype) in enumerate(types, 1):
-            print '\n DCT-{}: real to real discrete cosine transform {}'.format(
-                    stype.strip(), itype)
-            for (shape, _, _, N, _, _) in self.iter_shapes():
-                print '   DCT-{} shape={:9s} '.format(stype, str(shape)+':'),
-                if (itype==1): # real size is 2*(N-1)
-                    N += 1
-                    shape = mk_shape(shape, -1, shape[-1] + 1)
-                Href = np.random.rand(N).astype(dtype).reshape(shape)
-                results = {}
-                for (kind, implementations) in self.implementations.iteritems():
-                    for (name, impl) in implementations.iteritems():
-                        if dtype not in impl.supported_ftypes:
-                            continue
-                        if itype not in impl.supported_cosine_transforms:
-                            continue
-
-                        Bin = impl.backend.empty(shape=shape, dtype=dtype)
-                        plan = impl.dct(a=Bin, type=itype).setup()
+                    try:
+                        Bin = mk_buffer(backend=impl.backend, shape=cshape, dtype=ctype)
+                        plan = impl.irfft(a=Bin, n=shape[-1]).setup()
                         Bout = plan.output_array
                         assert Bout.shape == shape, self.msg_shape.format(shape, Bout.shape, 
                                 name)
@@ -240,20 +263,63 @@ class TestFFT(object):
                         H2 = Bout.get()
                         assert np.allclose(H1, H2, atol=eps), \
                                 self.msg_output_modified.format(name)
-                        results[name] = H1 / N
-                self.check_distances(results, eps, self.report_eps, 
+                        results[name] = H1
+                    except HysopFFTDataLayoutError as e:
+                        results[name] = e
+            check_distances(results, eps, self.report_eps, 'forced C2R', failures)
+    
+        types = ['I  ','II ','III','IV ']
+        for (itype,stype) in enumerate(types, 1):
+            print '\n DCT-{}: real to real discrete cosine transform {}'.format(
+                    stype.strip(), itype)
+            for (shape, cshape, rshape, N, Nc, Nr, 
+                    ghosts, mk_buffer) in self.iter_shapes():
+                print '   DCT-{} shape={:12s} ghosts={:12s} '.format(stype, shape, str(ghosts)+':'),
+                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 (kind, implementations) in self.implementations.iteritems():
+                    for (name, impl) in implementations.iteritems():
+                        if dtype not in impl.supported_ftypes:
+                            continue
+                        if itype not in impl.supported_cosine_transforms:
+                            continue
+                        try:
+                            Bin = mk_buffer(backend=impl.backend, shape=shape, dtype=dtype)
+                            plan = impl.dct(a=Bin, type=itype).setup()
+                            Bout = plan.output_array
+                            assert Bout.shape == shape, self.msg_shape.format(shape, Bout.shape, 
+                                    name)
+                            assert Bout.dtype == dtype, self.msg_dtype.format(dtype, Bout.dtype, 
+                                    name)
+                            Bin[...] = Href
+                            plan.execute()
+                            H0 = Bin.get()
+                            H1 = Bout.get()
+                            assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
+                            Bout[...] = 0
+                            evt = plan.execute()
+                            assert plan.output_array is Bout
+                            H2 = Bout.get()
+                            assert np.allclose(H1, H2, atol=eps), \
+                                    self.msg_output_modified.format(name)
+                            results[name] = H1 / shape[-1]
+                        except HysopFFTDataLayoutError as e:
+                            results[name] = e
+                check_distances(results, eps, self.report_eps, 
                         'DCT-{}'.format(stype), failures)
         
         for (itype,stype) in enumerate(types, 1):
             iitype = [1,3,2,4][itype-1]
-            print '\n IDCT-{}: real to real discrete cosine transform {}'.format(
+            print '\n IDCT-{}: real to real inverse discrete cosine transform {}'.format(
                     stype.strip(), itype)
-            for (shape, _, _, N, _, _) in self.iter_shapes():
-                print '   IDCT-{} shape={:9s} '.format(stype, str(shape)+':'),
+            for (shape, cshape, rshape, N, Nc, Nr, 
+                    ghosts, mk_buffer) in self.iter_shapes():
+                print '   IDCT-{} shape={:12s} ghosts={:12s} '.format(stype, shape, str(ghosts)+':'),
                 if (iitype==1): # real size is 2*(N-1)
-                    N += 1
                     shape = mk_shape(shape, -1, shape[-1] + 1)
-                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():
@@ -261,60 +327,118 @@ class TestFFT(object):
                             continue
                         if iitype not in impl.supported_cosine_transforms:
                             continue
-                        Bin = impl.backend.empty(shape=shape, dtype=dtype)
-                        plan = impl.idct(a=Bin, type=itype).setup()
-                        Bout = plan.output_array
-                        assert Bout.shape == shape, self.msg_shape.format(shape, Bout.shape, 
-                                name)
-                        assert Bout.dtype == dtype, self.msg_dtype.format(dtype, Bout.dtype, 
-                                name)
-                        Bin[...] = Href
-                        plan.execute()
-                        H0 = Bin.get()
-                        H1 = Bout.get()
-                        assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
-                        Bout[...] = 0
-                        evt = plan.execute()
-                        assert plan.output_array is Bout
-                        H2 = Bout.get()
-                        assert np.allclose(H1, H2, atol=eps), \
-                                self.msg_output_modified.format(name)
-                        results[name] = H1
-                self.check_distances(results, eps, self.report_eps, 
+                        try:
+                            Bin = mk_buffer(backend=impl.backend, shape=shape, dtype=dtype)
+                            plan = impl.idct(a=Bin, type=itype).setup()
+                            Bout = plan.output_array
+                            assert Bout.shape == shape, self.msg_shape.format(shape, Bout.shape, 
+                                    name)
+                            assert Bout.dtype == dtype, self.msg_dtype.format(dtype, Bout.dtype, 
+                                    name)
+                            Bin[...] = Href
+                            plan.execute()
+                            H0 = Bin.get()
+                            H1 = Bout.get()
+                            assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
+                            Bout[...] = 0
+                            evt = plan.execute()
+                            assert plan.output_array is Bout
+                            H2 = Bout.get()
+                            assert np.allclose(H1, H2, atol=eps), \
+                                    self.msg_output_modified.format(name)
+                            results[name] = H1
+                        except HysopFFTDataLayoutError as e:
+                            results[name] = e
+                check_distances(results, eps, self.report_eps, 
                         'IDCT-{}'.format(stype), failures)
 
-    
-    def check_distances(self, results, eps, report_eps, tag, failures):
-        if len(results.keys())==0:
-            print 'no support'
-            return
-        elif len(results.keys())==1:
-            impl = results.keys()[0]
-            print 'cannot compare'
-            return
-        ss=()
-        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))
-            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))
+        types = ['I  ','II ','III','IV ']
+        for (itype,stype) in enumerate(types, 1):
+            print '\n DST-{}: real to real discrete sine transform {}'.format(
+                    stype.strip(), itype)
+            for (shape, cshape, rshape, N, Nc, Nr, 
+                    ghosts, mk_buffer) in self.iter_shapes():
+                print '   DST-{} shape={:12s} ghosts={:12s} '.format(stype, shape, str(ghosts)+':'),
+                if (itype==1): # real size will be 2*(N+1)
+                    shape = mk_shape(shape, -1, shape[-1] - 1)
+                Href = np.random.rand(*shape).astype(dtype).reshape(shape)
+                results = {}
+                for (kind, implementations) in self.implementations.iteritems():
+                    for (name, impl) in implementations.iteritems():
+                        if dtype not in impl.supported_ftypes:
+                            continue
+                        if itype not in impl.supported_sine_transforms:
+                            continue
+                        try:
+                            Bin = mk_buffer(backend=impl.backend, shape=shape, dtype=dtype)
+                            plan = impl.dst(a=Bin, type=itype).setup()
+                            Bout = plan.output_array
+                            assert Bout.shape == shape, self.msg_shape.format(shape, Bout.shape, 
+                                    name)
+                            assert Bout.dtype == dtype, self.msg_dtype.format(dtype, Bout.dtype, 
+                                    name)
+                            Bin[...] = Href
+                            plan.execute()
+                            H0 = Bin.get()
+                            H1 = Bout.get()
+                            assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
+                            Bout[...] = 0
+                            evt = plan.execute()
+                            assert plan.output_array is Bout
+                            H2 = Bout.get()
+                            assert np.allclose(H1, H2, atol=eps), \
+                                    self.msg_output_modified.format(name)
+                            results[name] = H1 / shape[-1]
+                        except HysopFFTDataLayoutError as e:
+                            results[name] = e
+                check_distances(results, eps, self.report_eps, 
+                        'DST-{}'.format(stype), failures)
+        
+        for (itype,stype) in enumerate(types, 1):
+            iitype = [1,3,2,4][itype-1]
+            print '\n IDST-{}: real to real inverse discrete sine transform {}'.format(
+                    stype.strip(), itype)
+            for (shape, cshape, rshape, N, Nc, Nr, 
+                    ghosts, mk_buffer) in self.iter_shapes():
+                print '   IDST-{} shape={:12s} ghosts={:12s} '.format(stype, shape, str(ghosts)+':'),
+                if (iitype==1): # real size will be 2*(N+1)
+                    shape = mk_shape(shape, -1, shape[-1] - 1)
+                Href = np.random.rand(*shape).astype(dtype).reshape(shape)
+                results = {}
+                for (kind, implementations) in self.implementations.iteritems():
+                    for (name, impl) in implementations.iteritems():
+                        if dtype not in impl.supported_ftypes:
+                            continue
+                        if iitype not in impl.supported_sine_transforms:
+                            continue
+                        try:
+                            Bin = mk_buffer(backend=impl.backend, shape=shape, dtype=dtype)
+                            plan = impl.idst(a=Bin, type=itype).setup()
+                            Bout = plan.output_array
+                            assert Bout.shape == shape, self.msg_shape.format(shape, Bout.shape, 
+                                    name)
+                            assert Bout.dtype == dtype, self.msg_dtype.format(dtype, Bout.dtype, 
+                                    name)
+                            Bin[...] = Href
+                            plan.execute()
+                            H0 = Bin.get()
+                            H1 = Bout.get()
+                            assert np.array_equal(Href, H0), self.msg_input_modified.format(name)
+                            Bout[...] = 0
+                            evt = plan.execute()
+                            assert plan.output_array is Bout
+                            H2 = Bout.get()
+                            assert np.allclose(H1, H2, atol=eps), \
+                                    self.msg_output_modified.format(name)
+                            results[name] = H1
+                        except HysopFFTDataLayoutError as e:
+                            results[name] = e
+                check_distances(results, eps, self.report_eps, 
+                        'IDST-{}'.format(stype), failures)
+   
+
+
 
-        print ', '.join(ss)
-        if failed:
-            print
-            msg='Some implementations did not agree on the result !'
-            raise RuntimeError(msg)
-    
-    
     def _test_forward_backward_1d(self, dtype):
         print
         print '::Testing 1D forward-backward transforms, precision {}::'.format(dtype.__name__)
@@ -325,9 +449,16 @@ class TestFFT(object):
             failed = False
             ss=()
             for (name, Einf) in distances.iteritems():
-                Eeps = int(np.round(Einf/eps))
-                failed |= (Eeps >= self.fail_eps)
-                s='{}={}eps'.format(name,Eeps)
+                if isinstance(Einf, HysopFFTDataLayoutError):
+                    s='{}=UNSUPPORTED_STRIDES'.format(name)
+                elif np.isfinite(Einf):
+                    Eeps = int(np.round(Einf/eps))
+                    failed |= (Eeps >= self.fail_eps)
+                    s='{}={}eps'.format(name,Eeps)
+                else:
+                    Eeps = Einf
+                    failed |= True
+                    s='{}={}'.format(name,str(Eeps).upper())
                 ss += (s,)
             print ', '.join(ss)
             if failed:
@@ -336,157 +467,183 @@ class TestFFT(object):
                 raise RuntimeError(msg)
 
         print '\n C2C-C2C transform'
-        for (shape, cshape, rshape, N, Nc, Nr) in self.iter_shapes():
-            print '   X - IFFT(FFT(X)) shape={:9s} '.format(str(shape)+':'),
+        for (shape, cshape, rshape, N, Nc, Nr, 
+                ghosts, mk_buffer) in self.iter_shapes():
+            print '   X - IFFT(FFT(X)) shape={:12s} ghosts={:12s}'.format(shape, str(ghosts)+':'),
             Href = np.random.rand(2*N).astype(dtype).view(dtype=ctype).reshape(shape)
-            H0 = np.empty_like(Href)
-            H1 = np.empty_like(Href)
-            H0, H1, Href = self.simd_align(H0, H1, Href)
             results = {}
-            for (name, impl) in self.implementations.iteritems():
-                if dtype in impl.supported_ftypes:
-                    forward  = impl.fft(a=H0, out=H1)
-                    backward = impl.ifft(a=H1, out=H0)
-                    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():
+                    if dtype not in impl.supported_ftypes:
+                        continue
+                    D0 = mk_buffer(backend=impl.backend, shape=shape, dtype=ctype)
+                    D1 = mk_buffer(backend=impl.backend, shape=shape, dtype=ctype)
+                    try:
+                        forward  = impl.fft(a=D0, out=D1).setup()
+                        backward = impl.ifft(a=D1, out=D0).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 + 1j*np.nan
+                        forward.execute()
+                        D0[...] = np.nan + 1j*np.nan
+                        backward.execute()
+                        H0 = D0.get()
+                        results[name] = np.max(np.abs(Href-H0))
+                    except HysopFFTDataLayoutError as e:
+                        results[name] = e
             check_distances(results)
-        
+
         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)
-            H0 = np.empty(shape=shape,  dtype=dtype)
-            H1 = np.empty(shape=cshape, dtype=ctype)
-            H0, H1, Href = self.simd_align(H0, H1, Href)
+        for (shape, cshape, rshape, N, Nc, Nr,
+                ghosts, mk_buffer) in self.iter_shapes():
+            print '   X - IRFFT(RFFT(X)) shape={:12s} ghosts={:12s} '.format(shape, str(ghosts)+':'),
+            Href = np.random.rand(*shape).astype(dtype).reshape(shape)
             results = {}
-            for (name, impl) in self.implementations.iteritems():
-                if dtype in impl.supported_ftypes:
-                    forward  = impl.rfft(a=H0, out=H1)
-                    backward = impl.irfft(a=H1, out=H0)
-                    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():
+                    if dtype not in impl.supported_ftypes:
+                        continue
+                    D0 = mk_buffer(backend=impl.backend, shape=shape,  dtype=dtype)
+                    D1 = mk_buffer(backend=impl.backend, shape=cshape, dtype=ctype)
+                    try:
+                        forward  = impl.rfft(a=D0, out=D1).setup()
+                        backward = impl.irfft(a=D1, out=D0).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 + 1j*np.nan
+                        forward.execute()
+                        D0[...] = np.nan
+                        backward.execute()
+                        H0 = D0.get()
+                        results[name] = np.max(np.abs(Href-H0))
+                    except HysopFFTDataLayoutError as e:
+                        results[name] = e
             check_distances(results)
         
         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(
                     stype.strip(), itype)
             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)
+            for (shape, cshape, rshape, N, Nc, Nr,
+                    ghosts, mk_buffer) in self.iter_shapes():
+                print '   X - I{}({}(X)) shape={:12s} ghosts={:12s} '.format(ttype, ttype, shape, str(ghosts)+':'),
+                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 = mk_buffer(backend=impl.backend, shape=shape, dtype=dtype)
+                        D1 = mk_buffer(backend=impl.backend, shape=shape, dtype=dtype)
+                        try:
+                            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))
+                        except HysopFFTDataLayoutError as e:
+                            results[name] = e
                 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)
+            for (shape, cshape, rshape, N, Nc, Nr,
+                    ghosts, mk_buffer) in self.iter_shapes():
+                print '   X - I{}({}(X)) shape={:12s} ghosts={:12s} '.format(ttype, ttype, shape, str(ghosts)+':'),
+                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 = mk_buffer(backend=impl.backend, shape=shape, dtype=dtype)
+                        D1 = mk_buffer(backend=impl.backend, shape=shape, dtype=dtype)
+                        try:
+                            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))
+                        except HysopFFTDataLayoutError as e:
+                            results[name] = e
                 check_distances(results)
     
     def iter_shapes(self):
-        minj=(1,1)
+        minj=(3,3)
         maxj=(6,6)
-        # maxj=(12,8)
-        blacklists = ((), (7,))
         msg = ('EVEN', 'ODD')
+        def _mk_shape(shape, ghosts):
+            assert len(shape)==len(ghosts)
+            return tuple(Si+2*Gi for (Si,Gi) in zip(shape, ghosts))
+        def _mk_view(shape, ghosts):
+            assert len(shape)==len(ghosts)
+            return tuple(slice(Gi, Si+Gi) for (Si,Gi) in zip(shape, ghosts))
         for i in xrange(2):
             base = 2+i
             print '  '+msg[i]
-            for j1 in xrange(minj[i],maxj[i]):
-                if j1 in blacklists[i]:
-                    continue
-                shape = (base**j1,)
-                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)
-                Nr = np.prod(rshape, dtype=np.int64)
-                yield (shape, cshape, rshape, N, Nc, Nr)
+            for ghosts in ((0,0,0),): #(2,0,0),(0,1,0),(0,0,3)):
+                for j1 in xrange(minj[i],maxj[i]):
+                    shape = (3,2,base**j1,)
+                    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)
+                    Nr = np.prod(rshape, dtype=np.int64)
+                    def mk_buffer(backend, shape, dtype, ghosts=ghosts):
+                        real_shape = _mk_shape(shape=shape, ghosts=ghosts)
+                        view = _mk_view(shape=shape, ghosts=ghosts)
+                        buf = backend.empty(shape=real_shape, dtype=dtype)
+                        buf = buf[view]
+                        assert buf.shape == shape
+                        assert buf.dtype == dtype
+                        return buf
+                    yield (shape, cshape, rshape, N, Nc, Nr, ghosts, mk_buffer)
     
-    def check_distances(self, results, eps, report_eps, tag, failures):
-        if len(results.keys())==0:
-            print 'no support'
-            return
-        elif len(results.keys())==1:
-            impl = results.keys()[0]
-            print 'cannot compare'
-            return
-        ss=()
-        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))
-            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 and False:
-            print
-            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 =='
@@ -520,7 +677,7 @@ class TestFFT(object):
             raise RuntimeError
         else:
             msg =''
-            msg+='\n*************** FFT TESTS PASSED ******************'
+            msg+='\n************* ALL FFT TESTS PASSED ****************'
             msg+='\n** Some tests may have exceeded reporting error. **'
             msg+='\n***************************************************'
             msg+='\n'
@@ -531,8 +688,12 @@ 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,):
-            #self._test_forward_backward_1d(dtype=dtype)
+        if __ENABLE_LONG_TESTS__:
+            dtypes = (np.float32, np.float64,)
+        else:
+            dtypes = (HYSOP_REAL,)
+        for dtype in dtypes:
+            self._test_forward_backward_1d(dtype=dtype)
             self._test_1d(dtype=dtype, failures=failures.setdefault(dtype.__name__, {}))
         self.report_failures(failures)
     
@@ -541,5 +702,5 @@ if __name__ == '__main__':
 
     with printoptions(threshold=10000, linewidth=240, 
                       nanstr='nan', infstr='inf', 
-                      formatter={'float': lambda x: '{:>6.2f}'.format(x)}):
+                      formatter={'float': lambda x: '{:>0.2f}'.format(x)}):
         test.perform_tests()
diff --git a/hysop/old/operator.old/absorption_BC.py b/hysop/old/operator.old/absorption_BC.py
deleted file mode 100755
index 92c3142d6bea64b3449b226cdc6736c7c210b32f..0000000000000000000000000000000000000000
--- a/hysop/old/operator.old/absorption_BC.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Operator to kill the vorticity at the outlet boundary
-(i.e. removal of the periodic BC in the flow direction
-by vorticity absorption in order to set the far field
-velocity to u_inf at the inlet)
-
-"""
-from hysop.constants import debug
-from hysop.operator.discrete.absorption_BC import AbsorptionBC_D
-from hysop.operator.computational import Computational
-from hysop.domain.control_box import ControlBox
-from hysop.operator.continuous import opsetup
-
-
-class AbsorptionBC(Computational):
-    """
-    The periodic boundary condition is modified at the outlet
-    in the flow direction in order to discard 
-    in the dowstream region the eddies coming 
-    periodically from the oulet. 
-    The far field velocity is set to u_inf at the inlet.
-    """
-
-    @debug
-    def __init__(self, velocity, vorticity, req_flowrate,
-                 x_coords_absorp, **kwds):
-        """
-        @param[in] velocity field
-        @param[in, out] vorticity field to absorbe
-        @param[in] req_flowrate : required value for the flowrate 
-        (used to set the u_inf velocity value at the inlet)
-        @param x_coords_absorp : array containing the x-coordinates delimitating 
-        the absorption domain ([x_beginning, x_end])
-        """
-        assert 'variables' not in kwds, 'variables parameter is useless.'
-        super(AbsorptionBC, self).__init__(variables=[velocity,
-                                                        vorticity], **kwds)
-        ## velocity variable
-        self.velocity = velocity
-        ## vorticity variable
-        self.vorticity = vorticity
-
-        self.input = [self.velocity, self.vorticity]
-        self.output = [self.vorticity]
-        ## Expected value for the flow rate through input surface
-        self.req_flowrate = req_flowrate
-        ## x-coordinates delimitating the absorption band at the outlet
-        self.x_coords_absorp = x_coords_absorp
-        dom = self.velocity.domain
-        self.cb = ControlBox(origin=dom.origin, length=dom.length,
-                             parent=dom)
-        ## Extra parameters that may be required for discrete operator
-        ## (at the time, only io_params)
-        self.config = kwds
-
-    def discretize(self):
-        super(AbsorptionBC, self)._standard_discretize()
-        assert self._single_topo, 'Multi-resolution case is not allowed.'
-
-    @debug
-    @opsetup
-    def setup(self, rwork=None, iwork=None):
-        if not self._is_uptodate:
-            self.discrete_op =\
-                AbsorptionBC_D(self.discrete_fields[self.velocity],
-                               self.discrete_fields[self.vorticity],
-                               req_flowrate=self.req_flowrate, 
-                               x_coords_absorp=self.x_coords_absorp, 
-                               cb=self.cb, rwork=rwork, iwork=iwork)
-            # Output setup
-            self._set_io('absorption_BC', (1, 2 + self.domain.dim))
-            self.discrete_op.set_writer(self._writer)
-            self._is_uptodate = True
diff --git a/hysop/old/operator.old/absorption_bc.py b/hysop/old/operator.old/absorption_bc.py
deleted file mode 100755
index 2e23d8eb0b4b509a046f655223d0587020ffcb8b..0000000000000000000000000000000000000000
--- a/hysop/old/operator.old/absorption_bc.py
+++ /dev/null
@@ -1,126 +0,0 @@
-"""Operator to kill the vorticity at the outlet boundary
-(i.e. removal of the periodic BC in the flow direction
-by vorticity absorption in order to set the far field
-velocity to u_inf at the inlet)
-
-See :ref:`absorption_bc`.
-
-"""
-from hysop.constants import debug
-from hysop.operator.discrete.absorption_bc import AbsorptionBC as Dab
-from hysop.operator.computational import Computational
-from hysop.operator.continuous import opsetup
-from hysop.domain.subsets import SubBox
-import numpy as np
-
-
-class AbsorptionBC(Computational):
-    """
-    The periodic boundary condition is modified at the outlet
-    in the flow direction in order to discard
-    in the dowstream region the eddies coming
-    periodically from the outlet.
-    The far field velocity is set to u_inf at the inlet.
-    """
-
-    @debug
-    def __init__(self, velocity, vorticity, req_flowrate,
-                 x_range, filter_func=None, **kwds):
-        """
-        Parameters
-        ----------
-        velocity, vorticity : :class:`~hysop.fields.continuous_field.Field`
-        req_flowrate : double
-            required value for the flow rate
-            (used to set the u_inf velocity value at the inlet)
-        x_range : list or numpy array
-            x-coordinates delimitating the absorption domain
-            like [x_beginning, x_end]
-        filter_func: list of python functions, optional
-            functions used to compute the filter and its differential.
-        **kwds : extra parameters for base class
-
-
-        Notes
-        -----
-        * if set, filter_func[0] and filter_func[1] must be python function
-        returning a numpy array. For example to apply a sine inside
-        the absorption area use :
-
-        .. code::
-
-            def func(x):
-                return np.sin(x)
-
-        """
-        assert 'variables' not in kwds, 'variables parameter is useless.'
-        super(AbsorptionBC, self).__init__(variables=[velocity,
-                                                      vorticity], **kwds)
-        # velocity variable
-        self.velocity = velocity
-        # vorticity variable
-        self.vorticity = vorticity
-
-        self.input = [self.velocity, self.vorticity]
-        self.output = [self.vorticity]
-        # Expected value for the flow rate through input surface
-        self.req_flowrate = req_flowrate
-        # x-coordinates delimitating the absorption band at the outlet
-        self._filter_func = filter_func
-        self.absorption_box = self.build_absorption_box(x_range)
-        # on_proc[topo] = True if this operator has to work on the
-        # current process, i.e. if absorption_box has points
-        # on the current process.
-        self.on_proc = {}
-
-    def discretize(self):
-        super(AbsorptionBC, self)._standard_discretize()
-        assert self._single_topo, 'Multi-resolution case is not allowed.'
-        topo = self.discrete_fields[self.vorticity].topology
-        self.absorption_box.discretize(topo)
-        self.on_proc[topo] = self.absorption_box.on_proc[topo]
-
-    def build_absorption_box(self, x_range):
-        """Build a box to define the area where the absorption
-        filter will be applied
-
-        Parameters
-        ----------
-        x_range : list or numpy array
-            x right and left positions of the box.
-        """
-        # setup for the absorption filter definition
-        dom = self.vorticity.domain
-        borig = dom.origin.copy()
-        borig[0] = x_range[0]
-        blength = dom.length.copy()
-        blength[0] = x_range[1] - x_range[0]
-        return SubBox(parent=dom, origin=borig, length=blength)
-
-    def get_work_properties(self):
-        super(AbsorptionBC, self).get_work_properties()
-        wd = self.discrete_fields[self.vorticity]
-        subshape = self.absorption_box.mesh[wd.topology].resolution
-        subsize = np.prod(subshape)
-        return {'rwork': (subsize, ), 'iwork': None}
-
-    @debug
-    @opsetup
-    def setup(self, rwork=None, iwork=None):
-        if not self._is_uptodate:
-           # if self.on_proc[self.discrete_fields[self.vorticity].topology]:
-            self.discrete_op =\
-                Dab(velocity=self.discrete_fields[self.velocity],
-                    vorticity=self.discrete_fields[self.vorticity],
-                    req_flowrate=self.req_flowrate,
-                    absorption_box=self.absorption_box,
-                    rwork=rwork, iwork=iwork,
-                    filter_func=self._filter_func)
-
-            # Output setup
-            self._set_io('absorption_BC', (1, 2 + self.domain.dim))
-            self.discrete_op.set_writer(self._writer)
-            self._is_uptodate = True
-
-    def wait(self):
-        print "TEMP WAIT FOR TEST"
diff --git a/hysop/operator/adapt_timestep.py b/hysop/operator/adapt_timestep.py
index df80edfd669c79ccef0c61a560926354d0bf5f4d..fe66e08ecc75672920b7ce16ad8d20e1c7ef8e2e 100755
--- a/hysop/operator/adapt_timestep.py
+++ b/hysop/operator/adapt_timestep.py
@@ -73,12 +73,85 @@ class TimestepCriteria(ComputationalGraphOperator):
         pass
 
 
+class ConstantTimestepCriteria(TimestepCriteria):
+
+    @debug
+    def __init__(self, cst, parameter, Finf,
+                    name=None, pretty_name=None, **kwds):
+        """
+        Initialize a ConstantTimestepCriteria.
+
+        Compute a timestep criteria for an arbitrary field F.
+
+        dt = cst / Max_i(|Fi|inf) 
+          where i in [0, F.nb_components-1]
+
+        Parameters
+        ----------
+        cst: float or array-like of float
+            Constraint constant (per component, same shape as Finf).
+        Finf: ScalarParameter or TensorParameter
+            A tensor parameter that contains |F|inf for every considered components.
+        parameter: ScalarParameter
+            The output parameter that will store the computed timestep.
+        kwds: dict
+            Base class arguments.
+        """
+        if isinstance(cst, (int, long)):
+            cst = float(cst)
+        assert (cst > 0.0), 'negative cst factor.'
+        check_instance(cst, (float, npw.ndarray, list, tuple))
+        check_instance(Finf, (ScalarParameter, TensorParameter))
+        if isinstance(Finf, ScalarParameter):
+            assert isinstance(cst, float)
+            is_scalar = True
+        else:
+            is_scalar = False
+            if isinstance(cst, float):
+                cst = npw.full(shape=Finf.shape, dtype=Finf.dtype, fill_value=cst)
+            if isinstance(cst, (list, tuple)):
+                assert Finf.ndim == 1
+                cst = npw.asarray(cst)
+            msg='Shape mismatch between parameter {} and cst {}.'
+            msg=msg.format(Finf.shape, cst.shape)
+            assert Finf.shape == cst.shape, msg
+
+        name = first_not_None(name, 'CST')
+        pretty_name = first_not_None(pretty_name, name)
+        super(ConstantTimestepCriteria, self).__init__(name=name, pretty_name=pretty_name,
+                                                 input_params={Finf.name: Finf},
+                                                 output_params={parameter.name: parameter},
+                                                 parameter=parameter, **kwds)
+        self.cst = cst
+        self.Finf = Finf
+        self.is_scalar = is_scalar
+
+    def compute_criteria(self, **kwds):
+        cst  = self.cst
+        Finf = self.Finf()
+
+        if self.is_scalar:
+            assert Finf >= 0
+            if (Finf == 0):
+                return npw.inf
+            else:
+                return cst/Finf
+        else:
+            assert Finf.min() >= 0
+            mask = (Finf!=0)
+            dt = npw.full_like(cst, fill_value=npw.inf)
+            dt[mask] = cst[mask] / Finf[mask]
+            return dt.min()
+
 
 class CflTimestepCriteria(TimestepCriteria):
     
     @debug
-    def __init__(self, cfl, Finf, parameter, dx=None, 
-            name=None, pretty_name=None, **kwds):
+    def __init__(self, cfl, parameter,
+            Finf=None, Fmin=None, Fmax=None, 
+            dx=None, 
+            name=None, pretty_name=None, 
+            relative_velocities=None, **kwds):
         """
         Initialize a CflTimestepCriteria.
 
@@ -91,54 +164,114 @@ class CflTimestepCriteria(TimestepCriteria):
         cfl: float
             CFL value used to compute timestep.
         Finf: TensorParameter
-            A tensor parameter that contains |F|inf for every components.
+            A tensor parameter that contains Finf for every components.
+            Can be specified instead of Fmin and Fmax:
+                *Fmin will be set to -Finf
+                *Fmax will be set to +Finf
+        Fmin: TensorParameter
+            A tensor parameter that contains Fmin for every components.
+        Fmax: TensorParameter
+            A tensor parameter that contains Fmax for every components.
         parameter: ScalarParameter
             The output parameter that will store the computed timestep.
         dx: tuple of float
             Space discretization, should be of size F.nb_components.
             If not given, will be extracted from Finf on setup.
+        relative_velocities: array like of relative velocities, optional
+            Specify relative velocities.
         kwds: dict
             Base class arguments.
         """
         assert (cfl > 0.0), 'negative cfl condition.'
         check_instance(cfl, float)
-        check_instance(Finf, TensorParameter)
+        if (Finf is None):
+            check_instance(Fmin, TensorParameter)
+            check_instance(Fmax, TensorParameter)
+            assert Fmin.shape == Fmax.shape
+            input_params={ Fmin.name: Fmin, Fmax.name: Fmax } 
+            dtype = Fmin.dtype
+            shape = Fmin.shape
+            size  = Fmin.size
+        else:
+            check_instance(Finf, TensorParameter)
+            msg='Cannot specify (Fmin,Fmax) and Finf at the same time.'
+            assert (Fmin is None), msg
+            assert (Fmax is None), msg
+            input_params={ Finf.name: Finf } 
+            dtype = Finf.dtype
+            shape = Finf.shape
+            size  = Finf.size
+
         check_instance(parameter, ScalarParameter)
         check_instance(dx, tuple, values=float, allow_none=True)
+        check_instance(relative_velocities, (tuple, list), allow_none=True)
 
         name = first_not_None(name, 'CFL')
         pretty_name = first_not_None(pretty_name, name)
         super(CflTimestepCriteria,self).__init__(name=name, pretty_name=pretty_name,
-                                                 input_params={Finf.name: Finf}, 
+                                                 input_params=input_params,
                                                  output_params={parameter.name: parameter},
                                                  parameter=parameter, **kwds)
+
+        if (relative_velocities is None):
+            relative_velocities = [(0,)*size]
+        assert len(relative_velocities)>=1
+
+        rv = ()
+        for Vr in relative_velocities:
+            Vr = npw.asarray(Vr, dtype=dtype)
+            assert Vr.shape == shape
+            rv += (Vr,)
+        relative_velocities = rv
+
         self.cfl  = cfl
+        self.Fmin = Fmin
+        self.Fmax = Fmax
         self.Finf = Finf
         self.dx   = dx
+        self.relative_velocities = relative_velocities
 
     def setup(self, **kwds):
         super(CflTimestepCriteria, self).setup(**kwds)
         if (self.dx is None):
-            Finf = self.Finf
-            if hasattr(Finf, 'min_max_dfield'):
-                self.dx = Finf.min_max_dfield.get_unique_attribute('mesh', '_mesh', '_space_step')[::-1]
-            else:
-                msg='Could not extract dx from Finf.'
+            dx = None
+            for attr in ('Finf', 'Fmin', 'Fmax'):
+                attr = getattr(self, attr)
+                if hasattr(attr, 'min_max_dfield'):
+                    dx = attr.min_max_dfield.get_unique_attribute('mesh', '_mesh', '_space_step')[::-1]
+            if (dx is None):
+                msg='Could not extract dx from Fmin.'
                 raise RuntimeError(msg)
+            self.dx = dx
 
     def compute_criteria(self, **kwds):
         cfl = self.cfl
         dx = self.dx
-        Finf = tuple(self.Finf().tolist())
-        assert len(dx) == len(Finf)
-        if npw.any(npw.divide(Finf, dx)==0):
-            dt = cfl*npw.inf
+        if (self.Finf is None):
+            Fmin = self.Fmin()
+            Fmax = self.Fmax()
         else:
-            dt = cfl / npw.max(npw.divide(Finf, dx))
+            Finf = self.Finf()
+            Fmin = -Finf
+            Fmax = +Finf
+        assert len(dx) == Fmin.size == Fmax.size
+        assert len(self.relative_velocities)>=1
+
+        dt = npw.inf
+        for Vr in self.relative_velocities:
+            Vmin = Fmin - Vr
+            Vmax = Fmax - Vr
+            Vinf = npw.maximum(npw.abs(Vmin), npw.abs(Vmax))
+            if npw.all(npw.divide(Vinf, dx)==0):
+                cdt = cfl*npw.inf
+            else:
+                cdt = cfl / npw.max(npw.divide(Vinf, dx))
+            dt = min(dt, cdt)
         return dt
     
     def compute_cfl(self, dt):
-        return dt * npw.max(npw.divide(self.Finf(), self.dx))
+        mdt = self.compute_criteria()
+        return (dt / mdt) * self.cfl
 
 class AdvectionTimestepCriteria(TimestepCriteria):
 
@@ -239,7 +372,7 @@ class StretchingTimestepCriteria(TimestepCriteria):
 
         where |dFi/dXj| = |gradF|_inf_ij
 
-        ie. dt = cst / np.max( gradFinf.sum(axis=1) )
+        ie. dt = cst / npw.max( gradFinf.sum(axis=1) )
 
         Parameters
         ----------
@@ -369,18 +502,34 @@ class AdaptiveTimeStep(ComputationalGraphNodeGenerator):
         self.cfl_criteria = None
         self.parameter = dt
 
-    def push_cfl_criteria(self, cfl, Finf=None, dx=None,
+    def push_cst_criteria(self, cst, Finf=None,
+            name=None, pretty_name=None,
+            param_name=None, param_pretty_name=None,
+            parameter=None, quiet=False, **kwds):
+        
+        parameter = self._build_parameter(parameter=parameter, quiet=quiet,
+                name=param_name, pretty_name=param_pretty_name, 
+                basename=name.replace('dt_', ''))
+        criteria = ConstantTimestepCriteria(cst=cst, Finf=Finf,
+            parameter=parameter, name=name, pretty_name=pretty_name, **kwds)
+        self._push_criteria(parameter.name, criteria)
+
+    def push_cfl_criteria(self, cfl, Fmin=None, Fmax=None, Finf=None,
+            dx=None,
             name=None, pretty_name=None, 
             param_name=None, param_pretty_name=None,
             parameter=None, quiet=False,
+            relative_velocities=None,
             equivalent_CFL=None, **kwds):
         """
         See hysop.operator.adapt_timpestep.CflTimestepCriteria.
         """
         parameter = self._build_parameter(parameter=parameter, quiet=quiet,
                 name=param_name, pretty_name=param_pretty_name, basename='cfl')
-        criteria = CflTimestepCriteria(cfl=cfl, Finf=Finf, dx=dx, parameter=parameter, 
-                                        name=name, pretty_name=pretty_name, **kwds)
+        criteria = CflTimestepCriteria(cfl=cfl, Fmin=Fmin, Fmax=Fmax, Finf=Finf,
+                                        dx=dx, parameter=parameter, 
+                                        name=name, pretty_name=pretty_name, 
+                                        relative_velocities=relative_velocities, **kwds)
         self._push_criteria(parameter.name, criteria)
         
         if isinstance(equivalent_CFL, ScalarParameter):
@@ -394,7 +543,7 @@ class AdaptiveTimeStep(ComputationalGraphNodeGenerator):
         self.equivalent_CFL = equivalent_CFL
         self.cfl_criteria = cfl_criteria
         return criteria.dt
-    
+
     def push_advection_criteria(self, lcfl, criteria, Finf=None, gradFinf=None, 
             name=None, pretty_name=None, 
             param_name=None, param_pretty_name=None,
@@ -409,6 +558,12 @@ class AdaptiveTimeStep(ComputationalGraphNodeGenerator):
             name=name, pretty_name=pretty_name, **kwds)
         self._push_criteria(parameter.name, criteria)
         return criteria.dt
+    
+    def push_lcfl_criteria(self, *args, **kwds):
+        """
+        See hysop.operator.adapt_timpestep.AdvectionTimestepCriteria.
+        """
+        return self.push_advection_criteria(*args, **kwds)
 
     def push_stretching_criteria(self, criteria, gradFinf, cst=1.0,
             name=None, pretty_name=None, parameter=None, quiet=False, **kwds):
diff --git a/hysop/operator/base/curl.py b/hysop/operator/base/curl.py
new file mode 100644
index 0000000000000000000000000000000000000000..f6651ca5376fbf8f1ac27ac88901c8423f144363
--- /dev/null
+++ b/hysop/operator/base/curl.py
@@ -0,0 +1,243 @@
+
+
+
+from abc import abstractmethod
+from hysop.constants import SpectralTransformAction
+from hysop.tools.numpywrappers import npw
+from hysop.tools.types import check_instance, first_not_None, to_tuple
+from hysop.tools.decorators import debug
+from hysop.tools.numerics import float_to_complex_dtype
+from hysop.fields.continuous_field import Field
+from hysop.parameters.scalar_parameter import ScalarParameter
+from hysop.operator.base.spectral_operator import SpectralOperatorBase
+from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
+from hysop.fields.continuous_field import Field
+from hysop.symbolic.relational import Assignment
+from hysop.core.memory.memory_request import MemoryRequest
+
+class CurlOperatorBase(SpectralOperatorBase):
+    """
+    Compute the curl using a specific implementation.
+    """
+    
+    @debug
+    def __init__(self, Fin, Fout, variables, **kwds):
+        """
+        Create an operator that computes the curl of an input field Fin.
+
+        Given Fin, a 2D ScalarField, a 2D VectorField or a 3D VectorField, compute Fout = curl(Fin).
+        
+        Only the following configurations are supported:
+                 dim   nb_components  |   dim   nb_components
+        Input:    2        (1,2)      |    3          3
+        Output:   2        (2,1)      |    3          3
+
+        Parameters
+        ----------
+        Fin: hysop.field.continuous_field.Field
+            Continuous field as input ScalarField or VectorField.
+            All contained field have to live on the same domain.
+        Fout: hysop.field.continuous_field.Field
+            Continuous field as output VectorField.
+            All contained field have to live on the same domain.
+        variables: dict
+            dictionary of fields as keys and topologies as values.
+        kwds: dict, optional
+            Extra parameters passed towards base class (MultiSpaceDerivatives).
+        """
+        
+        check_instance(Fin,  Field)
+        check_instance(Fout, Field)
+        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
+
+        if (Fout.domain is not Fin.domain):
+            raise RuntimeError('Only one domain is supported.')
+        if (variables[Fout] != variables[Fin]):
+            raise RuntimeError('Only one topology is supported')
+        if (Fout.dtype != Fin.dtype):
+            raise RuntimeError('Datatype mismatch between Fout and Fin.')
+
+        # check fields
+        dim            = Fin.dim
+        in_components  = Fin.nb_components
+        out_components = Fout.nb_components
+        if (dim==2):
+            if(in_components==1):
+                if (out_components!=2):
+                    msg='Fout component mistmach, got {} components but expected 2.'.format(out_components)
+                    raise RuntimeError(msg)
+            elif(in_components==2):
+                if (out_components!=1):
+                    msg='Fout component mistmach, got {} components but expected 1.'.format(out_components)
+                    raise RuntimeError(msg)
+            else:
+                msg='Fin component mistmach, got {} components but expected 1 or 2.'.format(in_components)
+                raise RuntimeError(msg)
+        elif (dim==3):
+            if (in_components!=3):
+                msg='Fin component mistmach, got {} components but expected 3.'.format(in_components)
+                raise RuntimeError(msg)
+            if(out_components!=3):
+                msg='Fout component mistmach, got {} components but expected 3.'.format(out_components)
+                raise RuntimeError(msg)
+        else:
+            msg='Unsupported dimension {}.'.format(dim)
+            raise RuntimeError(msg)
+        
+        # input and output fields
+        input_fields  = { Fin:  variables[Fin] }
+        output_fields = { Fout: variables[Fout] }
+
+        super(CurlOperatorBase, self).__init__(input_fields=input_fields, 
+                output_fields=output_fields, **kwds)
+
+        self.Fin  = Fin
+        self.Fout = Fout
+        self.dim  = dim
+        
+    @debug
+    def discretize(self):
+        if self.discretized:
+            return
+        super(CurlOperatorBase, self).discretize()
+        self.dFin  = self.get_input_discrete_field(self.Fin)
+        self.dFout = self.get_output_discrete_field(self.Fout)
+
+
+
+class SpectralCurlOperatorBase(CurlOperatorBase):
+    """
+    Compute the curl using a specific spectral implementation.
+    """
+    def __init__(self, **kwds):
+        super(SpectralCurlOperatorBase, self).__init__(**kwds)
+        
+        dim = self.dim
+        Fin, Fout = self.Fin, self.Fout
+        assert Fin is not Fout, 'Cannot compute curl inplace!'
+        
+        if (dim==2):
+            tg0 = self.new_transform_group()
+            tg1 = self.new_transform_group()
+            if (Fin.nb_components==1):
+                assert (Fout.nb_components==2)
+                F0 = tg0.require_forward_transform(Fin, axes=0, 
+                        custom_output_buffer='auto')
+                B0 = tg0.require_backward_transform(Fout[0], axes=0,
+                        custom_input_buffer='auto', matching_forward_transform=F0)
+
+                F1 = tg1.require_forward_transform(Fin, axes=1,
+                        custom_output_buffer='auto')
+                B1 = tg1.require_backward_transform(Fout[1], axes=1,
+                        custom_input_buffer='auto', matching_forward_transform=F1)
+            
+                expr0 = Assignment(B0.s, +F0.s.diff(F0.s.frame.coords[1]))
+                expr1 = Assignment(B1.s, -F1.s.diff(F1.s.frame.coords[0]))
+            elif (Fin.nb_components==2):
+                F0 = tg0.require_forward_transform(Fin[1], axes=1,
+                        custom_output_buffer='auto')
+                B0 = tg0.require_backward_transform(Fout, axes=1,
+                        custom_input_buffer='auto', matching_forward_transform=F0)
+
+                F1 = tg1.require_forward_transform(Fin[0], axes=0,
+                        custom_output_buffer='auto')
+                B1 = tg1.require_backward_transform(Fout, axes=0, 
+                        custom_input_buffer='auto', matching_forward_transform=F1,
+                        action=SpectralTransformAction.ACCUMULATE)
+                
+                expr0 = Assignment(B0.s, +F0.s.diff(F0.s.frame.coords[0]))
+                expr1 = Assignment(B1.s, -F1.s.diff(F1.s.frame.coords[1]))
+            else:
+                raise NotImplementedError
+            K0, = tg0.push_expressions(expr0)
+            K1, = tg1.push_expressions(expr1)
+            K = ((tg0,K0),(tg1,K1))
+            forward_transforms  = (F0, F1)
+            backward_transforms = (B0, B1)
+        elif(dim==3):
+            tg0 = self.new_transform_group()
+            tg1 = self.new_transform_group()
+            tg2 = self.new_transform_group()
+            if (Fin.nb_components==3):
+                assert Fout.nb_components==3
+                F0 = tg1.require_forward_transform(Fin[2], axes=1,
+                        custom_output_buffer='auto')
+                B0 = tg1.require_backward_transform(Fout[0], axes=1,
+                        custom_input_buffer='auto', matching_forward_transform=F0)
+
+                F1 = tg0.require_forward_transform(Fin[0], axes=0,
+                        custom_output_buffer='auto')
+                B1 = tg0.require_backward_transform(Fout[1], axes=0,
+                        custom_input_buffer='auto', matching_forward_transform=F1)
+
+                F2 = tg2.require_forward_transform(Fin[1], axes=2,
+                        custom_output_buffer='auto')
+                B2 = tg2.require_backward_transform(Fout[2], axes=2,
+                        custom_input_buffer='auto', matching_forward_transform=F2)
+
+                F3 = tg0.require_forward_transform(Fin[1], axes=0,
+                        custom_output_buffer='auto')
+                B3 = tg0.require_backward_transform(Fout[0], axes=0,
+                        custom_input_buffer='auto', matching_forward_transform=F3,
+                        action=SpectralTransformAction.ACCUMULATE)
+
+                F4 = tg2.require_forward_transform(Fin[2], axes=2,
+                        custom_output_buffer='auto')
+                B4 = tg2.require_backward_transform(Fout[1], axes=2,
+                        custom_input_buffer='auto', matching_forward_transform=F4,
+                        action=SpectralTransformAction.ACCUMULATE)
+
+                F5 = tg1.require_forward_transform(Fin[0], axes=1,
+                        custom_output_buffer='auto')
+                B5 = tg1.require_backward_transform(Fout[2], axes=1,
+                        custom_input_buffer='auto', matching_forward_transform=F5,
+                        action=SpectralTransformAction.ACCUMULATE)
+            else:
+                raise NotImplementedError
+            expr0 = Assignment(B0.s, +F0.s.diff(F0.s.frame.coords[1]))
+            expr1 = Assignment(B1.s, +F1.s.diff(F1.s.frame.coords[2]))
+            expr2 = Assignment(B2.s, +F2.s.diff(F2.s.frame.coords[0]))
+            expr3 = Assignment(B3.s, -F3.s.diff(F3.s.frame.coords[2]))
+            expr4 = Assignment(B4.s, -F4.s.diff(F4.s.frame.coords[0]))
+            expr5 = Assignment(B5.s, -F5.s.diff(F5.s.frame.coords[1]))
+            K0, = tg1.push_expressions(expr0)
+            K1, = tg0.push_expressions(expr1)
+            K2, = tg2.push_expressions(expr2)
+            K3, = tg0.push_expressions(expr3)
+            K4, = tg2.push_expressions(expr4)
+            K5, = tg1.push_expressions(expr5)
+            K = ((tg1,K0),(tg0,K1),(tg2,K2),
+                 (tg0,K3),(tg2,K4),(tg1,K5))
+            forward_transforms  = (F0, F1, F2, F3, F4, F5)
+            backward_transforms = (B0, B1, B2, B3, B4, B5)
+        else:
+            raise NotImplementedError
+       
+        self.forward_transforms  = forward_transforms
+        self.backward_transforms = backward_transforms
+        self.K = K
+    
+    @debug
+    def discretize(self):
+        if self.discretized:
+            return
+        super(SpectralCurlOperatorBase, self).discretize()
+        dFin, dFout = self.dFin, self.dFout
+        K = self.K
+        
+        dK = ()
+        for (tg,Ki) in K:
+            _, dKi, _ = tg.discrete_wave_numbers[Ki]
+            dK += (dKi,)
+        self.dK = dK
+    
+    def setup(self, work):
+        super(SpectralCurlOperatorBase, self).setup(work)
+
+        # extract buffers
+        FIN  = tuple(Ft.full_output_buffer for Ft in self.forward_transforms)
+        FOUT = FIN
+
+        self.FIN  = FIN
+        self.FOUT = FOUT
+
diff --git a/hysop/operator/base/custom_symbolic_operator.py b/hysop/operator/base/custom_symbolic_operator.py
index a1d9ea104cb06af51c0f1df565cc13fad050b247..913530cd267ad84b2cf5a6d984a3cd31abdcc830 100644
--- a/hysop/operator/base/custom_symbolic_operator.py
+++ b/hysop/operator/base/custom_symbolic_operator.py
@@ -4,7 +4,7 @@ from hysop.deps import sm
 from hysop.tools.numpywrappers import npw
 from hysop.tools.types       import check_instance, to_tuple, InstanceOf, first_not_None, to_set
 from hysop.tools.decorators  import debug
-from hysop.tools.sympy_utils import get_derivative_variables
+from hysop.tools.sympy_utils import get_derivative_variables, SetupExprI
 from hysop.fields.continuous_field import Field
 from hysop.fields.discrete_field import DiscreteField, DiscreteScalarFieldView
 from hysop.fields.field_requirements import DiscreteFieldRequirements
@@ -35,6 +35,7 @@ from hysop.numerics.stencil.stencil_generator import StencilGenerator, CenteredS
 
 ValidExpressions = (Assignment,)
 
+
 class ExprDiscretizationInfo(object):
     SimpleCounterTypes  = (SymbolicArray, SymbolicBuffer,)
     IndexedCounterTypes = (DiscreteScalarFieldView,)
@@ -316,7 +317,7 @@ class SymbolicExpressionInfo(object):
         self.has_direction = (self.direction is not None)
         self.direction = first_not_None(self.direction, 0)
 
-    def discretize_expressions(self, input_dfields, output_dfields):
+    def discretize_expressions(self, input_dfields, output_dfields, force_symbolic_axes):
         check_instance(input_dfields,  dict, keys=Field, values=DiscreteScalarFieldView)
         check_instance(output_dfields, dict, keys=Field, values=DiscreteScalarFieldView)
         assert len(set(self.input_fields.keys())  - set(input_dfields.keys())) == 0
@@ -330,7 +331,9 @@ class SymbolicExpressionInfo(object):
         self.stencils = SortedDict()
 
         dfields  = input_dfields.values() + output_dfields.values()
-        if dfields:
+        if (force_symbolic_axes is not None):
+            axes = force_symbolic_axes
+        elif dfields:
             axes = dfields[0].tstate.axes
             for dfield in dfields:
                 if (dfield.tstate.axes != axes):
@@ -338,15 +341,22 @@ class SymbolicExpressionInfo(object):
                     msg+= 'with reference axes {}.'
                     msg = msg.format(dfield.name, dfield.tstate.axes, axes)
                     raise RuntimeError(msg)
-            self.axes = axes
         else:
             msg='No discrete fields found in custom symbolic operator.'
             raise RuntimeError(msg)
+<<<<<<< HEAD
+
+=======
+        self.axes = axes
 
+>>>>>>> master
         SymbolicExpressionParser.discretize_expressions(self)
 
         self.check_dfield_sizes()
 
+    def setup_expressions(self, work):
+        SymbolicExpressionParser.setup_expressions(self, work)
+
     def check_dfield_sizes(self):
         dfields = set(f for f in (self.input_dfields.values() + self.output_dfields.values()))
         if len(dfields)>0:
@@ -601,9 +611,9 @@ class SymbolicExpressionParser(object):
         if isinstance(expr, npw.ndarray):
             assert expr.ndim == 0
             expr = expr.tolist()
-        if isinstance(expr, (int,long,float,npw.number)):
+        if isinstance(expr, (str, int,long,float,complex,npw.number)):
             return
-        elif isinstance(expr, (AppliedSymbolicField, SymbolicScalarParameter)):
+        elif isinstance(expr, (AppliedSymbolicField, SymbolicScalarParameter, SymbolicArray)):
             cls.read(variables, info, expr)
         elif isinstance(expr, sm.Derivative):
             dvars = get_derivative_variables(expr)
@@ -667,6 +677,13 @@ class SymbolicExpressionParser(object):
                 raise ValueError(msg)
             if (field not in info.input_fields):
                 info.input_fields[field] = variables[field]
+        elif isinstance(var, SymbolicArray):
+            array = var
+            info.check_array(array)
+            if (array not in info.input_arrays):
+                info.input_arrays[array.name] = array
+            else:
+                assert info.input_arrays[array.name] is array
         elif isinstance(var, SymbolicScalarParameter):
             param = var.parameter
             if param.name in info.input_params:
@@ -848,7 +865,7 @@ class SymbolicExpressionParser(object):
             assert expr.ndim == 0
             expr = expr.tolist()
 
-        if isinstance(expr, (int, long, sm.Integer, float, sm.Rational, sm.Float, npw.number)):
+        if isinstance(expr, (int, long, sm.Integer, float, complex, sm.Rational, sm.Float, npw.number)):
             return {}
         elif isinstance(expr, SymbolicArray):
             return {expr: expr.new_requirements()}
@@ -857,6 +874,8 @@ class SymbolicExpressionParser(object):
             index = expr.index
             return {(field,index): DiscreteFieldRequirements(operator=None, variables=None,
                                                                 field=field, _register=False)}
+        elif isinstance(expr, str):
+            return {}
         elif isinstance(expr, sm.Derivative):
             dexpr = expr.args[0]
             dvars = get_derivative_variables(expr)
@@ -1011,13 +1030,15 @@ class SymbolicExpressionParser(object):
             assert expr.ndim == 0
             expr = expr.tolist()
         di = ExprDiscretizationInfo()
-        if isinstance(expr, (int,long,float,npw.number)):
+        if isinstance(expr, (int,long,float,complex,npw.number)):
             return expr, di
         elif cls.should_transpose_expr(info, expr):
             expr = cls.transpose_expr(info, expr)
             return expr, di
         elif isinstance(expr, TmpScalar):
             return expr, di
+        elif isinstance(expr, str):
+            return expr, di
         elif isinstance(expr, SymbolicScalarParameter):
             di.push_parameters(expr.parameter)
             return expr, di
@@ -1068,6 +1089,16 @@ class SymbolicExpressionParser(object):
             msg=msg.format(type(expr), type(expr).__mro__)
             raise NotImplementedError(msg)
 
+    @classmethod
+    def setup_expressions(cls, info, work):
+        check_instance(info, SymbolicExpressionInfo)
+        for dexpr in info.dexprs:
+            cls.setup_one(dexpr, work)
+
+    @classmethod
+    def setup_one(cls, dexpr, work):
+        for atom in dexpr.atoms(SetupExprI):
+            atom.setup(work)
 
     @classmethod
     def transposable_expressions(cls):
@@ -1308,14 +1339,15 @@ class CustomSymbolicOperatorBase(DirectionalOperatorBase):
         return requirements
 
     @debug
-    def discretize(self):
+    def discretize(self, force_symbolic_axes=None):
         """Discretize variables and symbolic expressions."""
         if self.discretized:
             return
         super(CustomSymbolicOperatorBase, self).discretize()
         self._expr_info.discretize_expressions(
                 input_dfields=self.input_discrete_fields,
-                output_dfields=self.output_discrete_fields)
+                output_dfields=self.output_discrete_fields,
+                force_symbolic_axes=force_symbolic_axes)
 
     @debug
     def setup(self, work):
diff --git a/hysop/operator/base/derivative.py b/hysop/operator/base/derivative.py
index 3ba644ca0ede5b721bf81f7dee2b0c802074501e..2f9541d162fe5fdf03ff0440050d3a3cf70fd705 100644
--- a/hysop/operator/base/derivative.py
+++ b/hysop/operator/base/derivative.py
@@ -1,18 +1,21 @@
 
-from abc import ABCMeta
+from abc import ABCMeta, abstractmethod
 
 from hysop.deps import sm
-from hysop.constants         import DirectionLabels
+from hysop.tools.numpywrappers import npw
 from hysop.tools.types       import check_instance, to_tuple, first_not_None, InstanceOf
 from hysop.tools.decorators  import debug
-from hysop.tools.numpywrappers import npw
-from hysop.fields.continuous_field import Field, ScalarField
+from hysop.tools.sympy_utils import exponent, subscript, partial
+from hysop.constants         import DirectionLabels, SpaceDiscretization
 from hysop.parameters.tensor_parameter import TensorParameter
+from hysop.symbolic.relational import Assignment
 
 from hysop.core.memory.memory_request import MemoryRequest
+from hysop.core.graph.graph import op_apply
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
-from hysop.constants import SpaceDiscretization
-from hysop.tools.sympy_utils import exponent, subscript, partial
+from hysop.fields.continuous_field import Field, ScalarField
+from hysop.operator.base.spectral_operator import SpectralOperatorBase
+
 
 class SpaceDerivativeBase(object):
     """
@@ -20,8 +23,6 @@ class SpaceDerivativeBase(object):
     """
 
     __metaclass__ = ABCMeta
-    __default_method = { SpaceDiscretization: 2 }
-    __available_methods = { SpaceDiscretization: InstanceOf(int) }
 
     @debug
     def __init__(self, F, dF, A=None, 
@@ -44,11 +45,14 @@ class SpaceDerivativeBase(object):
             Some backend may allow inplace differentiation.
         A: numerical value, ScalarParameter or ScalarField, optional
             Scaling for convenience, defaults to 1.
-        derivative: int, optional
-            Which derivative to generate, defaults to 1.
+        derivative: int or tuple, optional
+            Which derivative to generate, defaults to (0,)*(dim-1)+(1,).
+            ie. first order derivative in X axis.
+            If integer is given, the derivative is taken in given direction.
         direction: int, optional
             Directions in which to take the derivative.
-            Defaults to 0.
+            Defaults to None.
+            Should be None if derivative is a tuple.
         require_tmp: bool, optional
             Should this operator generate a tmp array ?
             Default to True if F is dF (inplace computation) else False.
@@ -60,36 +64,67 @@ class SpaceDerivativeBase(object):
             Pretty name of this operator.
         kwds: dict, optional
             Base class keyword arguments.
+        
+        Notes
+        -----
+        There is two way to build a derivative:
+            (1) derivative(int) + direction(int) gives:
+                 => derivative=(0,0,0,0,kd,0,0,0)
+                    where the index of kd is direction
+                    and kd=derivative
+            (2) derivative(tuple) + direction(None) gives:
+                 => derivative=(k0,...,kn)
         """
+        assert (derivative is not None)
         A          = first_not_None(A, 1)
-        derivative = first_not_None(derivative, 1)
-        direction  = first_not_None(direction, 0)
-        default_name = 'd{}{}/d{}{}'.format(derivative if (derivative>1) else '', 
-                                                  F.name, DirectionLabels[direction].lower(),
-                                                  '**{}'.format(derivative) if (derivative>1) else '')
-        dk   = exponent(derivative)
-        p    = partial
-        with_p = (len(F.pretty_name)>6)
-        default_pretty_name = u'{p}{lp}{}{}{rp}/{p}{}{}'.format(dk if (derivative>1) else u'', 
-                                                  F.pretty_name.decode('utf-8'), 
-                                                  DirectionLabels[direction].lower(),
-                                                  dk if (derivative>1) else u'', 
-                                                  p=partial,
-                                                  lp=u'(' if with_p else u'',
-                                                  rp=u')' if with_p else u'')
+
+        check_instance(F,  ScalarField)
+        check_instance(dF, ScalarField, allow_none=True)
+        
+        if isinstance(derivative, tuple):
+            assert len(derivative)==F.dim
+        else:
+            direction  = first_not_None(direction, 0)
+            _derivative = [0,]*F.dim
+            _derivative[direction] = derivative
+            derivative = tuple(_derivative)
+        check_instance(derivative, tuple, size=F.dim, minval=0)
+        
+        nz_derivatives = tuple(x for x in derivative if (x>0))
+        if len(nz_derivatives) == 1:
+            directional_derivative = nz_derivatives[0]
+            _direction = derivative.index(directional_derivative)
+            if (direction is not None):
+                assert _direction == direction
+            else:
+                direction = _direction
+        else:
+            assert (direction is None)
+            directional_derivative = None
+        
+        expr = F.s()
+        for (i,xi) in enumerate(F.domain.frame.coords):
+            if derivative[i]>0:
+                expr = expr.diff(xi, derivative[i])
+        _dF = F.from_sympy_expression(expr=expr, space_symbols=F.domain.frame.coords,
+                                        is_tmp=True)
+        default_name = _dF.name
+        default_pretty_name = _dF.pretty_name
+        if (dF is None):
+            dF = _dF
+
+        check_instance(derivative, tuple, size=F.dim, minval=0)
+        check_instance(direction, int, minval=0, maxval=F.dim-1, allow_none=True)
+        check_instance(directional_derivative, int, minval=0, allow_none=True)
+
         name = first_not_None(name, default_name)
         pretty_name = first_not_None(pretty_name, default_pretty_name)
         variables = first_not_None(variables, {})
-                                                    
-        check_instance(F,  ScalarField)
-        check_instance(dF, ScalarField)
-        check_instance(derivative, int, minval=1)
-        check_instance(direction, int, minval=0, maxval=F.dim-1)
+        
+        check_instance(name, str)
+        check_instance(pretty_name, (str, unicode))
         check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
-
-        assert 0 <= direction < F.dim, direction
-        assert 1 <= derivative, derivative
-
+        
         input_fields  = { F: variables.get(F, None) }
         output_fields = { dF: variables.get(dF, input_fields[F]) }
         input_params = {}
@@ -118,33 +153,13 @@ class SpaceDerivativeBase(object):
         self.Fout = dF
         self.A = A
         self.derivative  = derivative
+        self.directional_derivative = directional_derivative
         self.direction   = direction
         self.is_inplace  = is_inplace
         self.require_tmp = require_tmp
         self.scale_by_field = scale_by_field
         self.scale_by_parameter = scale_by_parameter
         self.scale_by_value = scale_by_value
-    
-    @classmethod
-    def default_method(cls):
-        dm = super(SpaceDerivativeBase, cls).default_method()
-        dm.update(cls.__default_method)
-        return dm
-    @classmethod
-    def available_methods(cls):
-        am = super(SpaceDerivativeBase, cls).available_methods()
-        am.update(cls.__available_methods)
-        return am
-    
-    @debug
-    def handle_method(self, method):
-        super(SpaceDerivativeBase, self).handle_method(method)
-        
-        if not hasattr(self, 'space_discretization'):
-            space_discretization = method.pop(SpaceDiscretization)
-            assert (2 <= space_discretization), space_discretization
-            assert (space_discretization % 2 == 0), space_discretization
-            self.space_discretization = space_discretization
      
     @debug
     def discretize(self):
@@ -164,7 +179,7 @@ class SpaceDerivativeBase(object):
         requests = super(SpaceDerivativeBase, self).get_work_properties()
         if self.require_tmp:
             request = MemoryRequest.empty_like(a=self.dFout, nb_components=1, 
-                    shape=self.dFout.compute_resolution)
+                    shape=self.dFout.compute_resolution, backend=self.backend)
             requests.push_mem_request('tmp', request)
         return requests
     
@@ -175,3 +190,87 @@ class SpaceDerivativeBase(object):
             raise ValueError('work is None.')
         if self.require_tmp:
             self.dtmp, = work.get_buffer(self, 'tmp')
+
+
+
+class FiniteDifferencesSpaceDerivativeBase(SpaceDerivativeBase):
+        
+    def __init__(self, **kwds):
+        super(FiniteDifferencesSpaceDerivativeBase, self).__init__(**kwds)
+
+        directional_derivative, direction = self.directional_derivative, self.direction
+        msg='FiniteDifferencesSpaceDerivative only supports directional derivatives.'
+        assert isinstance(direction, int), msg
+        assert isinstance(directional_derivative, int), msg
+        
+    @classmethod
+    def default_method(cls):
+        dm = super(FiniteDifferencesSpaceDerivativeBase, cls).default_method()
+        dm.update({ SpaceDiscretization: 2 })
+        return dm
+
+    @classmethod
+    def available_methods(cls):
+        am = super(FiniteDifferencesSpaceDerivativeBase, cls).available_methods()
+        am.update({ SpaceDiscretization: InstanceOf(int) })
+        return am
+    
+    @debug
+    def handle_method(self, method):
+        super(FiniteDifferencesSpaceDerivativeBase, self).handle_method(method)
+        
+        if not hasattr(self, 'space_discretization'):
+            space_discretization = method.pop(SpaceDiscretization)
+            assert (2 <= space_discretization), space_discretization
+            assert (space_discretization % 2 == 0), space_discretization
+            self.space_discretization = space_discretization
+    
+
+
+class SpectralSpaceDerivativeBase(SpectralOperatorBase, SpaceDerivativeBase):
+    @debug
+    def __init__(self, testing=False, **kwds):
+        kwds['require_tmp'] = False
+        super(SpectralSpaceDerivativeBase, self).__init__(**kwds)
+
+        F, dF = self.Fin, self.Fout
+        derivative = self.derivative
+        
+        tg = self.new_transform_group()
+
+        axes = tuple(i for (i,di) in enumerate(derivative[::-1]) if (di>0))
+        if testing and not axes:
+            axes = tuple(xrange(F.dim))
+        elif not axes:
+            msg='No transform axes found, got derivative={}.'.format(derivative)
+            raise RuntimeError(msg)
+        
+        Ft = tg.require_forward_transform(F, axes=axes, custom_output_buffer='auto')
+        Bt = tg.require_backward_transform(dF, axes=axes, custom_input_buffer='auto', 
+                                            matching_forward_transform=Ft)
+        assert (Ft.output_dtype == Bt.input_dtype)
+        
+        dFt = Ft.s
+        assert len(derivative)==F.dim
+        for (i,di) in enumerate(derivative):
+            if (di>0):
+                dFt = dFt.diff(Ft.s.all_vars[i], di)
+        expr = Assignment(Bt.s, dFt)
+        kds = tg.push_expressions(expr)
+        
+        self.Ft = Ft
+        self.Bt = Bt
+        self.tg = tg
+        self.kds = kds
+        self.expr = expr
+
+    def discretize(self):
+        super(SpectralSpaceDerivativeBase, self).discretize()
+        dkds, nd_dkds = (), ()
+        for kd in self.kds:
+            _, dkd, nd_dkd = self.tg.discrete_wave_numbers[kd]
+            dkds += (dkd,)
+            nd_dkds += (nd_dkd,)
+        self.dkds = dkds
+        self.nd_dkds = nd_dkds
+
diff --git a/hysop/operator/base/diffusion.py b/hysop/operator/base/diffusion.py
index 4b37c1678d0797fecc8d58a5bebf69be1311bc49..227a432572bf599b09fa8016b4d13016adcc78ea 100644
--- a/hysop/operator/base/diffusion.py
+++ b/hysop/operator/base/diffusion.py
@@ -1,20 +1,25 @@
-from hysop.tools.types       import check_instance
-from hysop.tools.decorators  import debug
-from hysop.tools.numpywrappers import npw
+from hysop.tools.types import check_instance, first_not_None
+from hysop.tools.decorators import debug
 from hysop.fields.continuous_field import Field
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
+from hysop.operator.base.poisson import PoissonOperatorBase
+from hysop.symbolic.relational import Assignment
+from hysop.symbolic.field import laplacian
 from hysop.parameters.scalar_parameter import ScalarParameter
-from hysop.operator.base.fft_operator import FftOperatorBase
 
-class DiffusionBase(FftOperatorBase):
+
+class DiffusionOperatorBase(PoissonOperatorBase):
     """
-    Solve the diffusion equation using a specific implementation.
+    Common base for spectral diffusion operator.
     """
 
     @debug
-    def __init__(self, Fin, Fout, 
-                       viscosity, variables, dt, **kargs):
-        """Diffusion operator base.
+    def __init__(self, Fin, Fout, variables,
+                       nu, dt, 
+                       name=None, pretty_name=None,
+                       **kwds):
+        """
+        Diffusion operator base.
 
         Parameters
         ----------
@@ -24,8 +29,8 @@ class DiffusionBase(FftOperatorBase):
             The output field to be diffused.
         variables: dictionary of fields:topology
             The choosed discretizations.
-        viscosity : float or ScalarParameter.
-            Viscosity value.
+        nu: ScalarParameter.
+            Diffusion coefficient.
         dt: ScalarParameter
             Timestep parameter that will be used for time integration.
         kargs:
@@ -37,61 +42,24 @@ class DiffusionBase(FftOperatorBase):
                 in  = Win
                 out = Wout
                 
-            *Implicit resolution in Fourier space:
+            *Implicit resolution in spectral space:
                 F_hat(tn+1) = 1/(1-nu*dt*sum(Ki**2)) * F_hat(tn)
         """
-        check_instance(Fin,  Field)
-        check_instance(Fout, Field)
-        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
-        check_instance(viscosity, (float, ScalarParameter))
+        check_instance(nu, ScalarParameter)
         check_instance(dt, ScalarParameter)
-        
-        assert Fin.nb_components == Fout.nb_components, \
-                'input and output components mismatch'
-        assert variables[Fin] == variables[Fout], \
-                'input and output topology mismatch'
-        assert Fin.domain is Fout.domain,\
-                'input and output domain mismatch'
 
-        input_fields   = { Fin:  variables[Fin] }
-        output_fields  = { Fout: variables[Fout] }
-        input_params   = { dt.name: dt }
-        if isinstance(viscosity, ScalarParameter):
-            input_params[viscosity.name] = viscosity
-        else:
-            self._real_viscosity = viscosity
-            viscosity = lambda: self._real_viscosity
+        input_params   = { dt.name: dt,
+                           nu.name: nu }
+        
+        default_name = 'Diffusion_{}_{}'.format(Fin.name, Fout.name)
+        default_pretty_name = 'Diffusion_{}_{}'.format(Fin.pretty_name, Fout.pretty_name)
+        name = first_not_None(name, default_name)
+        pretty_name = first_not_None(name, default_pretty_name)
 
-        super(DiffusionBase, self).__init__(input_fields=input_fields, 
-                                            output_fields=output_fields,
-                                            input_params=input_params,
-                                            **kargs)
+        super(DiffusionOperatorBase, self).__init__(
+                Fin=Fin, Fout=Fout, variables=variables,
+                name=name, pretty_name=pretty_name,
+                input_params=input_params, **kwds)
 
-        self.Fin   = Fin
-        self.Fout  = Fout
-        self.is_inplace = (Fin is Fout)
-        self.viscosity = viscosity
+        self.nu = nu
         self.dt = dt
-
-    @debug
-    def discretize(self):
-        if self.discretized:
-            return
-        super(DiffusionBase, self).discretize()
-        dFin  = self.get_input_discrete_field(self.Fin)
-        dFout = self.get_output_discrete_field(self.Fout)
-        assert npw.array_equal(dFin.compute_resolution, dFout.compute_resolution)
-        self.dFin = dFin
-        self.dFout = dFout
-
-        self.input_buffers = dFin.compute_buffers
-        self.output_buffers = dFout.compute_buffers
-
-        dim        = dFin.dim
-        resolution = dFin.compute_resolution
-        length     = dFin.domain.length
-        dtype      = dFin.dtype
-        self.generate_wave_numbers(dim, resolution, length, dtype)
-
-    def generate_wave_numbers(self, dim, resolution, length, dtype):
-        pass
diff --git a/hysop/operator/base/external_force.py b/hysop/operator/base/external_force.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee94138d958d956c469c93053ecbd371069652b0
--- /dev/null
+++ b/hysop/operator/base/external_force.py
@@ -0,0 +1,261 @@
+
+from abc import ABCMeta, abstractmethod
+from hysop.constants import SpectralTransformAction
+from hysop.tools.numpywrappers import npw
+from hysop.tools.types import check_instance, first_not_None, to_tuple
+from hysop.tools.decorators import debug
+from hysop.tools.numerics import float_to_complex_dtype
+from hysop.fields.continuous_field import Field, ScalarField
+from hysop.operator.base.spectral_operator import SpectralOperatorBase
+from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
+from hysop.fields.continuous_field import Field
+from hysop.symbolic.relational import Assignment
+from hysop.core.memory.memory_request import MemoryRequest
+from hysop.parameters.tensor_parameter import TensorParameter
+from hysop.parameters.scalar_parameter import ScalarParameter
+from hysop.tools.interface import NamedObjectI
+
+class ExternalForce(NamedObjectI):
+    """Interface to implement a custom external force."""
+    __metaclass__ = ABCMeta
+
+    def __init__(self, name, dim, Fext, **kwds):
+        super(ExternalForce, self).__init__(name=name, **kwds)
+
+        check_instance(dim, int)
+        self._dim  = dim
+        self._Fext = Fext
+
+        for f in self.input_fields():
+            msg='Expected field dimension to be {} but got {} from field {}.'
+            msg=msg.format(dim, f.dim, f.short_description())
+            assert f.dim == dim, msg
+
+    
+    @property
+    def Fext(self):
+        return self._Fext
+    @property
+    def dim(self):
+        return self._dim
+    @property
+    def diffusion(self):
+        return self._diffusion
+
+    @abstractmethod
+    def input_fields(self):
+        pass
+    @abstractmethod
+    def output_fields(self):
+        pass
+    @abstractmethod
+    def input_params(self):
+        pass
+    @abstractmethod
+    def output_params(self):
+        pass
+
+    @abstractmethod
+    def initialize(self, op):
+        pass
+    @abstractmethod
+    def discretize(self, op):
+        pass
+    @abstractmethod
+    def get_mem_requests(self, op):
+        pass
+    @abstractmethod
+    def pre_setup(self, op, work):
+        pass
+    @abstractmethod
+    def post_setup(self, op, work):
+        pass
+    @abstractmethod
+    def apply(self, op, **kwds):
+        pass
+
+
+class SpectralExternalForceOperatorBase(SpectralOperatorBase):
+    """
+    Compute the curl of a symbolic expression and perfom Euler time integration.
+    """
+    
+    @debug
+    def __init__(self, vorticity, Fext, dt, variables,
+                    Fmin=None, Fmax=None, Finf=None,
+                    implementation=None, **kwds):
+        """
+        Create an operator that computes the curl of a given input force field Fext.
+
+        Only the following configurations are supported:
+                     dim   nb_components  |   dim   nb_components
+        vorticity:    2          1        |    3          3
+
+        What is computed:
+            force  = curl(Fext) by using a spectral backend
+            Fmin = min(force)
+            Fmax = max(force)
+            Finf = max(abs(Fmin), abs(Fmax))
+            W   += dt*force
+        
+        where Fext is computed from user given ExternalForce.
+
+        Parameters
+        ----------
+        vorticity: hysop.field.continuous_field.Field
+            Continuous field as input ScalarField or VectorField.
+            All contained field have to live on the same domain.
+        Fext: hysop.operator.external_force.ExternalForce
+            Expression of the external force.
+        F...: TensorParameter or boolean, optional
+            TensorParameters should match the shape of tmp (see Notes).
+            If set to True, the TensorParameter will be generated automatically.
+        variables: dict
+            dictionary of fields as keys and topologies as values.
+        kwds: dict, optional
+            Extra parameters passed towards base class (MultiSpaceDerivatives).
+
+        Notes
+        -----
+        If dim == 2, it is expected that:
+            vorticity has only one component
+            Fext has 2 components
+        Else if dim == 3:
+            vorticity has 3 components
+            Fext has 3 components
+        """
+        check_instance(vorticity, Field)
+        check_instance(Fext, ExternalForce)
+        check_instance(dt, ScalarParameter)
+        check_instance(Fmin, (ScalarParameter,TensorParameter), allow_none=True)
+        check_instance(Fmax, (ScalarParameter,TensorParameter), allow_none=True)
+        check_instance(Finf, (ScalarParameter,TensorParameter), allow_none=True)
+        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
+        
+        if (Fmin is not None):
+            Fmin.value = npw.asarray((1e8,)*vorticity.nb_components, dtype=Fmin.dtype)
+        if (Fmax is not None):
+            Fmax.value = npw.asarray((1e8,)*vorticity.nb_components, dtype=Fmax.dtype)
+        if (Finf is not None):
+            Finf.value = npw.asarray((1e8,)*vorticity.nb_components, dtype=Finf.dtype)
+
+        # check fields
+        dim           = vorticity.dim
+        w_components  = vorticity.nb_components
+        f_components  = Fext.dim
+
+        if (dim==2):
+            if(w_components!=1):
+                msg='Vorticity components mistmach, got {} components but expected 1.'
+                msg=msg.format(w_components)
+                raise RuntimeError(msg)
+            if(f_components!=2):
+                msg='External force components mistmach, got {} components but expected 2.'
+                msg=msg.format(f_components)
+                raise RuntimeError(msg)
+            pshape = (1,)
+        elif (dim==3):
+            if(w_components!=3):
+                msg='Vorticity components mistmach, got {} components but expected 3.'
+                msg=msg.format(w_components)
+                raise RuntimeError(msg)
+            if(f_components!=3):
+                msg='External force components mistmach, got {} components but expected 3.'
+                msg=msg.format(f_components)
+                raise RuntimeError(msg)
+            pshape = (3,)
+        else:
+            msg='Unsupported dimension {}.'.format(dim)
+            raise RuntimeError(msg)
+            
+        msg='TensorParameter shape mismatch, expected {} but got {{}} for parameter {{}}.'
+        msg=msg.format(pshape)
+        if isinstance(Fmin, TensorParameter):
+            assert Fmin.shape==pshape, msg.format(Fmin.shape, 'Fmin')
+        if isinstance(Fmax, TensorParameter):
+            assert Fmin.shape==pshape, msg.format(Fmax.shape, 'Fmax')
+        if isinstance(Finf, TensorParameter):
+            assert Fmin.shape==pshape, msg.format(Finf.shape, 'Finf')
+
+        compute_statistics  = (Fmin is not None)
+        compute_statistics |= (Fmax is not None)
+        compute_statistics |= (Finf is not None)
+
+        # input and output fields
+        input_fields = Fext.input_fields()
+        check_instance(input_fields,  set, values=ScalarField)
+        input_fields.add(vorticity)
+
+        output_fields = Fext.output_fields()
+        check_instance(output_fields, set, values=ScalarField)
+        output_fields.add(vorticity)
+
+        input_params = Fext.input_params()
+        input_params.add(dt)
+
+        output_params = Fext.output_params()
+        output_params.update({Fmin,Fmax,Finf})
+        output_params = output_params.difference({None})
+
+        input_fields  = {f: self.get_topo_descriptor(variables, f) for f in input_fields}
+        output_fields = {f: self.get_topo_descriptor(variables, f) for f in output_fields}
+        input_params  = {p.name: p for p in input_params}
+        output_params = {p.name: p for p in output_params}
+        
+        # TODO share tmp buffers for the whole tensor
+        force = vorticity.tmp_like(name='Fext', ghosts=0, mem_tag='tmp_fext')
+        for (Fi, Wi) in zip(force.fields, vorticity.fields):
+            input_fields[Fi]  = self.get_topo_descriptor(variables, Wi)
+            output_fields[Fi] = self.get_topo_descriptor(variables, Wi)
+
+        super(SpectralExternalForceOperatorBase, self).__init__(
+                input_fields=input_fields, output_fields=output_fields, 
+                input_params=input_params, output_params=output_params,
+                **kwds)
+
+        self.vorticity = vorticity
+        self.Fext  = Fext
+        self.force = force
+        self.dt = dt
+
+        self.dim = dim
+        self.w_components = w_components
+        self.f_components = f_components
+        
+        self.Fmin = Fmin
+        self.Fmax = Fmax
+        self.Finf = Finf
+        self.compute_statistics  = compute_statistics
+        
+################### 
+# from now on, we delegate everything to the ExternalForce implementation
+###################
+    def initialize(self, **kwds):
+        if self.initialized:
+            return
+        self.Fext.initialize(self)
+        return super(SpectralExternalForceOperatorBase, self).initialize(**kwds)
+        
+    @debug
+    def discretize(self):
+        if self.discretized:
+            return
+        super(SpectralExternalForceOperatorBase, self).discretize()
+        self.dW = self.get_input_discrete_field(self.vorticity)
+        self.dF = self.get_input_discrete_field(self.force)
+        self.Fext.discretize(self)
+
+    def get_work_properties(self):
+        requests = super(SpectralExternalForceOperatorBase, self).get_work_properties()
+        for (name, request) in self.Fext.get_mem_requests(self).iteritems():
+            requests.push_mem_request(name, request)
+        return requests
+    
+    def setup(self, work):
+        self.Fext.pre_setup(self, work)
+        super(SpectralExternalForceOperatorBase, self).setup(work)
+        self.Fext.post_setup(self, work)
+    
+    def apply(self, **kwds):
+        self.Fext.apply(self, **kwds)
+
diff --git a/hysop/operator/base/fft_operator.py b/hysop/operator/base/fft_operator.py
deleted file mode 100644
index 0909254dafc8ccb96378c7edc4d2c54f8c461c3b..0000000000000000000000000000000000000000
--- a/hysop/operator/base/fft_operator.py
+++ /dev/null
@@ -1,26 +0,0 @@
-
-from abc import ABCMeta
-
-from hysop.tools.types       import check_instance, to_tuple
-from hysop.tools.decorators  import debug
-from hysop.fields.continuous_field import Field
-from hysop.tools.numerics import is_fp, is_complex
-
-from hysop.core.memory.memory_request import MemoryRequest
-from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
-
-class FftOperatorBase(object):
-    """
-    Common implementation interface for FFT based operators.
-    """
-
-    __metaclass__ = ABCMeta
-
-    @debug
-    def __init__(self, **kwds):
-        """
-        Initialize a FFT based operator.
-        kwds: dict
-            Base class keyword arguments.
-        """
-        super(FftOperatorBase, self).__init__(**kwds)
diff --git a/hysop/operator/base/integrate.py b/hysop/operator/base/integrate.py
index 983e46f31ce17a389b923f553a72602d1314379e..710ee46fe76c9320e56849f7542330235ed5e218 100644
--- a/hysop/operator/base/integrate.py
+++ b/hysop/operator/base/integrate.py
@@ -20,7 +20,7 @@ class IntegrateBase(object):
 
     @debug
     def __init__(self, field, variables, 
-                    name=None, pretty_name=None,
+                    name=None, pretty_name=None, cst=1,
                     parameter=None, scaling=None, **kwds):
         """
         Initialize a Integrate operator base.
@@ -50,6 +50,8 @@ class IntegrateBase(object):
             'normalize': scale by first integration (first value will be 1.0) 
             Can also be a custom float value of tuple of float values.
             Defaults to volumic integration.
+        cst: float, optional
+            Extra scaling constant for volumic mode.
         kwds:
             Extra keywords arguments that will be passed towards implementation 
             enstrophy operator __init__.
@@ -87,6 +89,7 @@ class IntegrateBase(object):
         self.field     = field
         self.parameter = parameter
         self.scaling   = scaling
+        self.cst = cst
         self.scaling_coeff = None
         
         super(IntegrateBase, self).__init__(name=name, pretty_name=pretty_name,
@@ -101,7 +104,7 @@ class IntegrateBase(object):
 
         scaling = self.scaling
         if (scaling == 'volumic'):
-            scaling_coeff = npw.prod(dF.space_step) / npw.prod(dF.domain.length)
+            scaling_coeff = self.cst*npw.prod(dF.space_step) / npw.prod(dF.domain.length)
             scaling_coeff = (scaling_coeff,)*dF.nb_components
         elif (scaling == 'normalize'):
             scaling_coeff = [None,]*dF.nb_components
diff --git a/hysop/operator/base/min_max.py b/hysop/operator/base/min_max.py
index 0bbed36412c78c2f184581afe5b517c4810e1e79..964e2fcf83ed6d462ee1e3dca751ce362421cd3a 100644
--- a/hysop/operator/base/min_max.py
+++ b/hysop/operator/base/min_max.py
@@ -27,7 +27,7 @@ class MinMaxFieldStatisticsBase(object):
 
     @classmethod
     def build_parameters(cls, field, components, all_quiet,
-                    Fmin, Fmax, Finf, pbasename, ppbasename):
+                    Fmin, Fmax, Finf, pbasename, ppbasename, dtype=None):
         if (    ((Fmin is None) or (Fmin is False)) 
             and ((Fmax is None) or (Fmax is False))
             and ((Finf is None) or (Finf is False))):
@@ -37,8 +37,10 @@ class MinMaxFieldStatisticsBase(object):
             msg+=' tensor parameter.'
             raise ValueError(msg)
         
-        pbasename  = first_not_None(pbasename,  field.name)
-        ppbasename = first_not_None(ppbasename, field.pretty_name.decode('utf-8'))
+        if (field is not None):
+            dtype      = first_not_None(dtype,      field.dtype)
+            pbasename  = first_not_None(pbasename,  field.name)
+            ppbasename = first_not_None(ppbasename, field.pretty_name.decode('utf-8'))
         
         def make_param(k, quiet):
             return TensorParameter(name=names[k], pretty_name=pretty_names[k],
@@ -56,7 +58,9 @@ class MinMaxFieldStatisticsBase(object):
             'Finf': u'|{}|\u208a'.format(ppbasename),
         }
         
-        components = to_tuple(first_not_None(components, range(field.nb_components)))
+        if (field is not None):
+            components = first_not_None(components, range(field.nb_components))
+        components = to_tuple(components)
         nb_components = len(components)
 
         parameters = {}
diff --git a/hysop/operator/base/poisson.py b/hysop/operator/base/poisson.py
index 0ac4e4f6b8ec62c8df5a9f2d620b5f339bc76e6f..e7cd56d10b8de894dc819c49809d819d8b54e38e 100644
--- a/hysop/operator/base/poisson.py
+++ b/hysop/operator/base/poisson.py
@@ -1,22 +1,23 @@
 
-from abc import abstractmethod
-from hysop.tools.numpywrappers import npw
 from hysop.tools.types import check_instance, first_not_None
 from hysop.tools.decorators import debug
 from hysop.fields.continuous_field import Field
-from hysop.operator.base.fft_operator import FftOperatorBase
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
+from hysop.operator.base.spectral_operator import SpectralOperatorBase
+from hysop.symbolic.relational import Assignment
+from hysop.symbolic.field import laplacian
 
-class PoissonOperatorBase(FftOperatorBase):
+class PoissonOperatorBase(SpectralOperatorBase):
     """
     Solves the poisson equation using a specific implementation.
     """
     
     @debug
     def __init__(self, Fin, Fout, variables, 
-            extra_input_kwds=None, **kwds): 
+            name=None, pretty_name=None,
+            **kwds): 
         """
-        Initialize a n-dimensional Poisson operator base.
+        Initialize a n-dimensional Poisson operator base (using spectral methods).
 
         Solves:
             Laplacian(Fout) = Fin
@@ -26,62 +27,66 @@ class PoissonOperatorBase(FftOperatorBase):
         Fout: Field
             Input continuous field (rhs).
         Fin: Field
-            Output continuous field (lhs), possibly inplace.
+            Output continuous field (lhs), possibly inplace, same number of components as Fin.
         variables: dict
             Dictionary of fields as keys and topology descriptors as values.
         kwds: dict, optional
             Base class arguments.
         """
-        check_instance(Fin, Field)
+        check_instance(Fin,  Field)
         check_instance(Fout, Field)
         check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
         assert Fin.nb_components == Fout.nb_components
         
-        input_fields  = { Fin: variables[Fin]  }
+        input_fields  = { Fin:  variables[Fin]  }
         output_fields = { Fout: variables[Fout] }
+        
+        default_name = 'Poisson_{}_{}'.format(Fin.name, Fout.name)
+        default_pretty_name = 'Poisson_{}_{}'.format(Fin.pretty_name, Fout.pretty_name)
+        name = first_not_None(name, default_name)
+        pretty_name = first_not_None(name, default_pretty_name)
+
+        super(PoissonOperatorBase, self).__init__(name=name, pretty_name=pretty_name,
+                input_fields=input_fields, output_fields=output_fields, **kwds)
+
+        forward_transforms  = ()
+        backward_transforms = ()
+        wave_numbers        = ()
+        
+        tg = self.new_transform_group()
+        for (Fi,Fo) in zip(Fin.fields, Fout.fields):
+            Ft = tg.require_forward_transform(Fi, custom_output_buffer='auto')
+            Bt = tg.require_backward_transform(Fo, custom_input_buffer='auto',
+                                                matching_forward_transform=Ft)
+            assert (Ft.output_dtype == Bt.input_dtype)
+            expr = Assignment(Bt.s, laplacian(Ft.s, Ft.s.frame))
+            kds  = tg.push_expressions(expr)
+            
+            forward_transforms  += (Ft,)
+            backward_transforms += (Bt,)
+            wave_numbers        += (kds,)
 
-        super(PoissonOperatorBase, self).__init__(input_fields=input_fields, output_fields=output_fields, **kwds)
         self.Fin  = Fin
         self.Fout = Fout
+        self.tg   = tg
+        self.forward_transforms  = forward_transforms
+        self.backward_transforms = backward_transforms
+        self.wave_numbers        = wave_numbers
     
     @debug
     def discretize(self):
-        if self.discretized:
-            return
         super(PoissonOperatorBase, self).discretize()
-        dFin  = self.input_discrete_fields[self.Fin]
-        dFout = self.output_discrete_fields[self.Fout]
-        assert dFin.domain.domain is dFout.domain.domain
-        
-        in_buffers  = tuple(data[dFin.compute_slices] for data in dFin.buffers)
-        out_buffers = tuple(data[dFout.compute_slices] for data in dFout.buffers)
-        
-        dim = dFin.dim
-        resolution = dFin.compute_resolution
-        length     = dFin.domain.length
-        dtype      = dFin.dtype
-        axes       = npw.arange(dim)[::-1]
-        for (ib,ob) in zip(in_buffers, out_buffers):
-            assert ib.dtype == dtype, ib.dtype
-            assert ob.dtype == dtype, ib.dtype
-            assert npw.array_equal(npw.argsort(ib.strides), axes)
-            assert npw.array_equal(npw.argsort(ob.strides), axes)
-
-        self.dFin  = dFin
-        self.dFout = dFout
-        self.in_buffers  = in_buffers
-        self.out_buffers = out_buffers
-
-        self.dim = dim
-        self.dtype = dtype
-        self.axes = axes
-        self.resolution = resolution
-        self.dtype = dtype
-        self.backend = dFin.backend
-
-        self.generate_wave_numbers(dim, resolution, length, dtype, axes)
-
-    @abstractmethod
-    def generate_wave_numbers(self, dim, resolution, length, dtype, axes):
-        pass
+        self.dFin  = self.get_input_discrete_field(self.Fin)
+        self.dFout = self.get_output_discrete_field(self.Fout)
+        all_dkds, all_nd_dkds = (), ()
+        for wn in self.wave_numbers:
+            dkds, nd_dkds = [None,]*len(wn), [None,]*len(wn)
+            for wi in wn:
+                idx, dkd, nd_dkd = self.tg.discrete_wave_numbers[wi]
+                dkds[idx]    = dkd
+                nd_dkds[idx] = nd_dkd
+            all_dkds += (dkds,)
+            all_nd_dkds += (all_dkds,)
+        self.all_dkds    = all_dkds
+        self.all_nd_dkds = all_nd_dkds
 
diff --git a/hysop/operator/base/poisson_curl.py b/hysop/operator/base/poisson_curl.py
new file mode 100644
index 0000000000000000000000000000000000000000..44ae7c2eaf85e99ad68cd71d11ac6d7e8cf96a3d
--- /dev/null
+++ b/hysop/operator/base/poisson_curl.py
@@ -0,0 +1,324 @@
+
+from abc import abstractmethod
+from hysop.constants import FieldProjection
+from hysop.tools.numpywrappers import npw
+from hysop.tools.types import check_instance, first_not_None, to_tuple
+from hysop.tools.decorators import debug
+from hysop.tools.numerics import float_to_complex_dtype
+from hysop.fields.continuous_field import Field
+from hysop.parameters.scalar_parameter import ScalarParameter
+from hysop.operator.base.spectral_operator import SpectralOperatorBase
+from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
+from hysop.fields.continuous_field import Field
+from hysop.symbolic.field import laplacian, grad
+from hysop.symbolic.relational import Assignment
+from hysop.core.memory.memory_request import MemoryRequest
+
+class PoissonCurlOperatorBase(object):
+    """
+    Solves the poisson-rotational equation using a specific implementation.
+    """
+    
+    @debug
+    def __init__(self, vorticity, velocity, variables, 
+            diffusion=None, dt=None, projection=None, **kwds): 
+        """
+        PoissonCurl operator to solve incompressible flows using various fft backends.
+        
+        Parameters
+        ----------
+        velocity : :class:`~hysop.fields.continuous_field.Field
+            Output solution velocity field.
+        vorticity:  :class:`~hysop.fields.continuous_field.Field`
+            Input vorticity to be diffused, projected.
+            If diffused and/or projected, vorticity is also an output.
+        variables: dict
+            Dictionary of Fields as keys and topologies as values.
+        diffusion: ScalarParameter, optional, defaults to None.
+            Diffuse the vorticity field before applying projection and poisson velocity.
+        dt: ScalarParameter, optional, defaults to None
+            Timestep is only required for diffusion.
+            If diffusion is not enabled, this parameter is ignored.
+        projection: hysop.constants.FieldProjection or positive integer, optional
+            Project vorticity such that resolved velocity is divergence free (for 3D fields).
+            When active, projection is done prior to every solve, unless projection is 
+            an integer in which case it is done every given steps.
+            This parameter is ignored for 2D fields and defaults to no projection.
+        kwds : 
+            Base class parameters.
+        """
+        
+        check_instance(velocity,  Field)
+        check_instance(vorticity, Field)
+        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
+        check_instance(diffusion, ScalarParameter, allow_none=True)
+        check_instance(dt, ScalarParameter, allow_none=True)
+        check_instance(projection, (FieldProjection, int), allow_none=True)
+
+        assert velocity.domain is vorticity.domain, 'only one domain is supported'
+        assert variables[velocity] is variables[vorticity], 'only one topology is supported'
+
+        # check for diffusion
+        should_diffuse = (diffusion is not None)
+        if should_diffuse:
+            if (dt is None):
+                msg='Diffusion has been specified but no timestep was given.'
+                raise RuntimeError(msg)
+        else:
+            dt = None
+        
+        # check for projection
+        if (projection == FieldProjection.NONE) or (projection is None) \
+                or (projection==0) or (velocity.dim!=3):
+            projection = FieldProjection.NONE
+        should_project = (projection is not FieldProjection.NONE)
+        
+        if (projection == FieldProjection.NONE):
+            do_project = lambda simu: False
+        elif (projection == FieldProjection.EVERY_STEP):
+            do_project = lambda simu: True
+        else: # projection is an integer representing frequency 
+            freq = projection
+            if (freq>=1):
+                do_project = lambda simu: ((simu.current_iteration % freq)==0)
+            else:
+                msg='Got negative projection frequency {}.'.format(freq)
+                raise ValueError(msg)
+        
+        # check fields
+        dim=velocity.dim
+        wcomp = vorticity.nb_components
+        if (dim==2) and (wcomp!=1):
+            msg='Vorticity component mistmach, got {} components but expected 1.'.format(wcomp)
+            raise RuntimeError(msg)
+        if (dim==3) and (wcomp!=3):
+            msg='Vorticity component mistmach, got {} components but expected 3.'.format(wcomp)
+            raise RuntimeError(msg)
+        if (dim!=3) and (projection!=FieldProjection.NONE):
+            raise RuntimeError('Velocity projection only available in 3D.')
+        if (velocity.dtype != vorticity.dtype):
+            raise RuntimeError('Datatype mismatch between velocity and vorticity.')
+        
+        # input and output fields
+        vtopology = variables[velocity]
+        wtopology = variables[vorticity]
+        input_params  = {}
+        input_fields  = { vorticity: wtopology }
+        output_fields = { velocity:  vtopology }
+        if should_diffuse:
+            assert (dt is not None), 'Diffusion timestep has not been given.'
+            input_params[diffusion.name] = diffusion
+            input_params[dt.name] = dt
+        if (should_diffuse or should_project):
+            output_fields[vorticity] = wtopology
+
+        super(PoissonCurlOperatorBase, self).__init__(input_fields=input_fields, 
+                output_fields=output_fields, input_params=input_params, **kwds)
+
+        self.W = vorticity
+        self.U = velocity
+        self.dim = dim
+        
+        self.should_diffuse = should_diffuse
+        self.nu = diffusion
+        self.dt = dt
+
+        self.should_project = should_project
+        self.projection = projection
+        self.do_project = do_project
+    
+    @debug
+    def discretize(self):
+        if self.discretized:
+            return
+        super(PoissonCurlOperatorBase, self).discretize()
+        self.dW = self.get_input_discrete_field(self.W)
+        self.dU = self.get_output_discrete_field(self.U)
+        
+
+class SpectralPoissonCurlOperatorBase(PoissonCurlOperatorBase, SpectralOperatorBase):
+    @debug
+    def __init__(self, vorticity, velocity, variables, 
+            diffusion=None, dt=None, projection=None, **kwds): 
+
+        super(SpectralPoissonCurlOperatorBase, self).__init__(
+                vorticity=vorticity, velocity=velocity, variables=variables,
+                diffusion=diffusion, dt=dt, projection=projection, **kwds)
+
+        dim = self.dim
+        should_diffuse, should_project = self.should_diffuse, self.should_project
+        
+
+        # build spectral transforms
+        tg = self.new_transform_group()
+        W_forward_transforms  = to_tuple(tg.require_forward_transform(vorticity))
+        U_backward_transforms = to_tuple(tg.require_backward_transform(velocity,
+                                                            custom_input_buffer='B0'))
+        if (should_diffuse or should_project):
+            W_backward_transforms = to_tuple(tg.require_backward_transform(vorticity))
+        else:
+            W_backward_transforms = (None,)*dim
+        
+        W_Fts = npw.asarray([x.s for x in W_forward_transforms])
+        U_Bts = npw.asarray([x.s for x in U_backward_transforms])
+        W_Bts = npw.asarray([None if (x is None) else x.s for x in W_backward_transforms])
+
+        # generate wavenumbers
+        kd1s = ()
+        for Wi in W_Fts:
+            expr1 = grad(Wi, Wi.frame)
+            kd1 = sorted(tg.push_expressions(*to_tuple(expr1)), key=lambda k: k.axis)
+            kd1s += (kd1,)
+
+        # laplacian
+        kd2s = ()
+        for Wi in W_Fts:
+            expr2 = laplacian(Wi, Wi.frame)
+            kd2 = sorted(tg.push_expressions(*to_tuple(expr2)), key=lambda k: k.axis)
+            kd2s += (kd2,)
+
+        # curl 
+        if (dim==2):
+            W2,    = W_forward_transforms
+            U0, U1 = U_backward_transforms
+            exprs = (Assignment(U0.s, +W2.s.diff(W2.s.frame.coords[1])),
+                     Assignment(U1.s, -W2.s.diff(W2.s.frame.coords[0])))
+            Uin  = (W2, W2)
+            Uout = (U0, U1)
+            Uk   = tuple(tg.push_expressions(e)[0] for e in exprs)
+        elif (dim==3):
+            W0, W1, W2 = W_forward_transforms
+            U0, U1, U2 = U_backward_transforms
+            exprs = (Assignment(U0.s, +W2.s.diff(W2.s.frame.coords[1])),
+                     Assignment(U0.s, -W1.s.diff(W1.s.frame.coords[2])),
+                     Assignment(U1.s, +W0.s.diff(W0.s.frame.coords[2])),
+                     Assignment(U1.s, -W2.s.diff(W2.s.frame.coords[0])),
+                     Assignment(U2.s, +W1.s.diff(W1.s.frame.coords[0])),
+                     Assignment(U2.s, -W0.s.diff(W0.s.frame.coords[1])))
+            Uin  = (W2, W1, W0, W2, W1, W0)
+            Uout = (U0, U0, U1, U1, U2, U2)
+            Uk   = tuple(tg.push_expressions(e)[0] for e in exprs)
+        else: 
+            raise NotImplementedError
+        
+        self.tg = tg
+        self.W_forward_transforms  = W_forward_transforms
+        self.U_backward_transforms = U_backward_transforms
+        self.W_backward_transforms = W_backward_transforms
+        
+        self.W_Fts = W_Fts
+        self.U_Bts = U_Bts
+        self.W_Bts = W_Bts
+
+        self.kd1s = kd1s
+        self.kd2s = kd2s
+
+        self.Uin  = Uin
+        self.Uout = Uout
+        self.Uk   = Uk
+    
+    @debug
+    def discretize(self):
+        if self.discretized:
+            return
+        super(SpectralPoissonCurlOperatorBase, self).discretize()
+        
+        kd1s, kd2s = self.kd1s, self.kd2s
+        if self.should_project:
+            dkd1s = ()
+            for kd1 in kd1s:
+                dkd1 = [None,]*len(kd1)
+                for wi in kd1:
+                    idx, dkd, nd_dkd = self.tg.discrete_wave_numbers[wi]
+                    dkd1[idx] = dkd
+                dkd1s += (tuple(dkd1),)
+        else:
+            dkd1s = None
+
+        dkd2s = ()
+        for kd2 in kd2s:
+            dkd2 = [None,]*len(kd2)
+            for wi in kd2:
+                idx, dkd, nd_dkd = self.tg.discrete_wave_numbers[wi]
+                dkd2[idx] = dkd
+            dkd2s += (tuple(dkd2),)
+        
+        dUk = ()
+        for ki in self.Uk:
+            _, dki, _ = self.tg.discrete_wave_numbers[ki]
+            dUk += (dki,)
+        
+        self.dkd1s = dkd1s
+        self.dkd2s = dkd2s
+        self.dUk   = dUk
+    
+    def get_work_properties(self):
+        requests = super(SpectralPoissonCurlOperatorBase, self).get_work_properties()
+        Ut = self.U_backward_transforms
+        dtypes = tuple(tr.input_dtype for tr in Ut)
+        shapes = tuple(tr.input_shape for tr in Ut)
+        assert all(d==dtypes[0] for d in dtypes)
+        # we request one buffer per vorticity component (1 in 2D, 3 in 3D)
+        for (i,(Ft,Bt)) in enumerate(zip(self.W_forward_transforms,
+                                         self.W_backward_transforms)):
+            assert Ft.output_dtype == dtypes[0]
+            if (Bt is not None):
+                assert (Ft.backend is Bt.backend)
+                assert (Ft.output_dtype == Bt.input_dtype), (Ft.output_dtype, Bt.input_dtype)
+                assert (Ft.output_shape == Bt.input_shape), (Ft.output_shape, Bt.input_shape)
+            shape = Ft.output_shape
+            dtype = Ft.output_dtype
+            request = MemoryRequest(backend=self.tg.backend, dtype=dtype, 
+                                    shape=shape, nb_components=1,
+                                    alignment=self.min_fft_alignment)
+            requests.push_mem_request('fft_buffer_{}'.format(i), request)
+        return requests
+
+    
+    def setup(self, work):
+        dim = self.dim
+        Ks, KKs = self.dkd1s, self.dkd2s
+
+        W_forward_transforms  = self.W_forward_transforms 
+        W_backward_transforms = self.W_backward_transforms
+        U_backward_transforms = self.U_backward_transforms
+
+        for (i,(W_Ft,W_Bt)) in enumerate(zip(W_forward_transforms, 
+                                             W_backward_transforms)):
+            dtmp, = work.get_buffer(self, 'fft_buffer_{}'.format(i))
+            W_Ft.configure_output_buffer(dtmp)
+            if (W_Bt is not None):
+                W_Bt.configure_input_buffer(dtmp)
+            output_axes = W_Ft.output_axes
+
+        super(SpectralPoissonCurlOperatorBase, self).setup(work)
+
+        # extract buffers
+        reorder_fields = tuple(dim-1-i for i in output_axes)
+        if (dim==2):
+            K   = Ks
+            KK  = KKs
+            WIN = (W_forward_transforms[0].full_output_buffer,)
+        elif (dim==3):
+            K   = tuple(Ks[i]  for i in reorder_fields) if Ks else None
+            KK  = tuple(KKs[i] for i in reorder_fields)
+            WIN = tuple(W_forward_transforms[i].full_output_buffer for i in reorder_fields)
+        else:
+            raise NotImplementedError
+        self.WIN  = WIN
+        self.K  = K
+        self.KK = KK
+                
+        UIN, UOUT, UK = (), (), ()
+        assert len(self.Uin) == len(self.Uout) == len(self.dUk)
+        for i,(Uin, Uout, Uk) in enumerate(zip(self.Uin, self.Uout, self.dUk)):
+            Uin  = Uin.full_output_buffer
+            Uout = Uout.full_input_buffer
+            UIN  += (Uin,)
+            UOUT += (Uout,)
+            UK   += (Uk,)
+        
+        self.UIN  = UIN
+        self.UOUT = UOUT
+        self.UK   = UK
+
diff --git a/hysop/operator/base/poisson_rotational.py b/hysop/operator/base/poisson_rotational.py
deleted file mode 100644
index 6cd72b0569ee2560987acb94065e3acf19b5c4f3..0000000000000000000000000000000000000000
--- a/hysop/operator/base/poisson_rotational.py
+++ /dev/null
@@ -1,130 +0,0 @@
-
-from abc import abstractmethod
-from hysop.tools.numpywrappers import npw
-from hysop.tools.types import check_instance, first_not_None
-from hysop.tools.decorators import debug
-from hysop.tools.numerics import float_to_complex_dtype
-from hysop.fields.continuous_field import Field
-from hysop.operator.base.fft_operator import FftOperatorBase
-from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
-from hysop.constants import FieldProjection
-from hysop.fields.continuous_field import Field
-
-class PoissonRotationalOperatorBase(FftOperatorBase):
-    """
-    Solves the poisson-rotational equation using a specific implementation.
-    """
-    
-    @debug
-    def __init__(self, vorticity, velocity, variables, 
-            projection=None, **kwds): 
-        """PoissonRotational operator to solve incompressible flows using FFTW in Fortran.
-        
-        Parameters
-        ----------
-        velocity : :class:`~hysop.fields.continuous_field.Field
-            solution field
-        vorticity:  :class:`~hysop.fields.continuous_field.Field`
-            right-hand side
-        variables: dict
-            dictionary of fields as keys and topologies as values.
-        projection: hysop.constants.FieldProjection or positive integer, optional
-            Project vorticity such that resolved velocity is divergence free (for 3D fields).
-            When active, projection is done prior to every solve, unless projection is 
-            an integer in which case it is done every n applies.
-            This parameter is ignored for 2D fields and defaults to no projection.
-        kwds : 
-            base class parameters.
-        """
-        
-        check_instance(velocity,  Field)
-        check_instance(vorticity, Field)
-        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
-
-        assert velocity.domain is vorticity.domain, 'only one domain is supported'
-        assert variables[velocity] is variables[vorticity], 'only one topology is supported'
-
-        vtopology = variables[velocity]
-        wtopology = variables[vorticity]
-        input_fields  = { vorticity: wtopology }
-        output_fields = { velocity:  vtopology }
-        
-        if (projection == FieldProjection.NONE) or (projection is None) \
-                or (projection==0) or (velocity.dim==2):
-            projection = FieldProjection.NONE
-            output_fields[vorticity] = wtopology
-        else:
-            assert (velocity.dim==3) and (vorticity.dim==3), 'Projection only available in 3D.'
-            output_fields[vorticity] = wtopology
-        
-        super(PoissonRotationalOperatorBase, self).__init__(input_fields=input_fields, 
-                output_fields=output_fields, **kwds)
-        
-        dim=velocity.dim
-        wcomp = vorticity.nb_components
-        if (dim==2) and (wcomp!=1):
-            msg='Vorticity component mistmach, got {} components but expected 1.'.format(wcomp)
-            raise RuntimeError(msg)
-        if (dim==3) and (wcomp!=3):
-            msg='Vorticity component mistmach, got {} components but expected 3.'.format(wcomp)
-            raise RuntimeError(msg)
-        if (dim!=3) and (projection!=FieldProjection.NONE):
-            raise ValueError('Velocity reprojection only available in 3D.')
-        
-        if (projection == FieldProjection.NONE):
-            self._do_project = lambda simu: False
-        elif (projection == FieldProjection.EVERY_STEP):
-            self._do_project = lambda simu: True
-        else: # projection is an integer representing frequency 
-            freq = projection
-            if (freq>=1):
-                self._do_project = lambda simu: ((simu.current_iteration % freq)==0)
-            else:
-                msg='Got negative reprojection frequency {}.'.format(freq)
-                raise ValueError(msg)
-        
-        dtype = vorticity.dtype
-        ctype = float_to_complex_dtype(dtype)
-        
-        self.W = vorticity
-        self.U = velocity
-        self.projection = projection
-        self.dim = velocity.dim
-        self.dtype = dtype
-        self.ctype = ctype
-    
-    @debug
-    def discretize(self):
-        if self.discretized:
-            return
-        super(PoissonRotationalOperatorBase, self).discretize()
-        dW = self.get_input_discrete_field(self.W)
-        dU = self.get_output_discrete_field(self.U)
-       
-        W_buffers = dW.compute_buffers
-        U_buffers = dU.compute_buffers
-        
-        dtype, ctype = self.dtype, self.ctype
-        dim        = dU.dim
-        resolution = dU.compute_resolution
-        length     = dU.domain.length
-        axes       = npw.arange(dim)[::-1]
-        for b in (W_buffers + U_buffers):
-            assert (b.dtype == dtype), b.dtype
-        
-        self.dW = dW
-        self.dU = dU
-        self.W_buffers = W_buffers
-        self.U_buffers = U_buffers
-        
-        self.dtype = dtype
-        self.ctype = ctype
-        self.axes  = axes
-        self.resolution = resolution
-        self.dtype = dtype
-        self.backend = dW.backend
-
-        self.generate_wave_numbers(dim, resolution, length, dtype, ctype, axes)
-
-    def generate_wave_numbers(self, dim, resolution, length, dtype, ctype, axes):
-        pass
diff --git a/hysop/operator/base/redistribute_operator.py b/hysop/operator/base/redistribute_operator.py
index 1c1cedc1270766ed96b747244c6cecbd101675cb..c4aded49e3cd7c99ce39753bd51ad036e8c38003 100644
--- a/hysop/operator/base/redistribute_operator.py
+++ b/hysop/operator/base/redistribute_operator.py
@@ -25,7 +25,8 @@ class RedistributeOperatorBase(ComputationalGraphOperator):
         """
         pass
     
-    def supported_backends(self):
+    @classmethod
+    def supported_backends(cls):
         """
         return the backends that this operator's topologies can support.
         """
diff --git a/hysop/operator/base/solenoidal_projection.py b/hysop/operator/base/solenoidal_projection.py
index 08abe273ac2fdec0a765577579bc95f6eede250c..1e6880b88a3c3af1270428dcd97d666d231e44bb 100644
--- a/hysop/operator/base/solenoidal_projection.py
+++ b/hysop/operator/base/solenoidal_projection.py
@@ -1,16 +1,17 @@
 
 from abc import abstractmethod
 from hysop.tools.numpywrappers import npw
-from hysop.tools.types import check_instance, first_not_None
+from hysop.tools.types import check_instance, first_not_None, to_tuple
 from hysop.tools.decorators import debug
 from hysop.tools.numerics import float_to_complex_dtype
-from hysop.fields.continuous_field import Field
-from hysop.operator.base.fft_operator import FftOperatorBase
-from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
 from hysop.constants import FieldProjection
+from hysop.core.memory.memory_request import MemoryRequest
+from hysop.operator.base.spectral_operator import SpectralOperatorBase
+from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
 from hysop.fields.continuous_field import Field
+from hysop.symbolic.field import laplacian, div, curl, grad
 
-class SolenoidalProjectionOperatorBase(FftOperatorBase):
+class SolenoidalProjectionOperatorBase(SpectralOperatorBase):
     """
     Solves solenoidal projection (project a 3d field F such that div(F)=0)
     """
@@ -84,73 +85,144 @@ class SolenoidalProjectionOperatorBase(FftOperatorBase):
         super(SolenoidalProjectionOperatorBase, self).__init__(input_fields=input_fields, 
                 output_fields=output_fields, **kwds)
         
-        
-        dtype = input_field.dtype
-        ctype = float_to_complex_dtype(dtype)
-        
+        Fin  = input_field
+        Fout = output_field
+        compute_divFin  = (input_field_div is not None)
+        compute_divFout = (output_field_div is not None)
+
+        tg = self.new_transform_group()
+        forward_transforms  = tg.require_forward_transform(Fin)
+        backward_transforms = tg.require_backward_transform(Fout)
+
+        Fts = npw.asarray([x.s for x in forward_transforms])
+        Bts = npw.asarray([x.s for x in backward_transforms])
+
+        kd1s, kd2s = (), ()
+        for Wi in Fts:
+            expr = grad(Wi, Wi.frame)
+            kd1 = sorted(tg.push_expressions(*to_tuple(expr)), key=lambda k: k.axis)
+            expr = laplacian(Wi, Wi.frame)
+            kd2 = sorted(tg.push_expressions(expr), key=lambda k: k.axis)
+            kd1s += (kd1,)
+            kd2s += (kd2,)
+
+        if compute_divFin:
+            backward_divFin_transform = tg.require_backward_transform(input_field_div,
+                                            custom_input_buffer='B0')
+        else:
+            backward_divFin_transform = None
+
+        if compute_divFout:
+            backward_divFout_transform = tg.require_backward_transform(output_field_div,
+                                            custom_input_buffer='B0')
+        else:
+            backward_divFout_transform = None
+
         self.Fin     = input_field
         self.Fout    = output_field
         self.divFin  = input_field_div
         self.divFout = output_field_div
         
-        self.compute_divFin  = (input_field_div is not None)
-        self.compute_divFout = (output_field_div is not None)
-
-        self.dim  = output_field.dim
-        self.dtype = dtype
-        self.ctype = ctype
+        self.compute_divFin  = compute_divFout
+        self.compute_divFout = compute_divFout
+        
+        self.tg = tg
+        self.forward_transforms  = forward_transforms
+        self.backward_transforms = backward_transforms
+        self.backward_divFin_transform  = backward_divFin_transform
+        self.backward_divFout_transform = backward_divFout_transform
+        
+        self.Bts   = Bts
+        self.Fts   = Fts
+        self.kd1s  = kd1s
+        self.kd2s  = kd2s
 
     @debug
     def discretize(self):
         if self.discretized:
             return
         super(SolenoidalProjectionOperatorBase, self).discretize()
-        dFin     = self.get_input_discrete_field(self.Fin)
-        dFout    = self.get_output_discrete_field(self.Fout)
+        dFin  = self.get_input_discrete_field(self.Fin)
+        dFout = self.get_output_discrete_field(self.Fout)
         
-        Fin_buffers  = tuple(data[dFin.compute_slices] for data in dFin.buffers)
-        Fout_buffers = tuple(data[dFout.compute_slices] for data in dFout.buffers)
-
         if self.compute_divFin:
             ddivFin  = self.output_discrete_fields[self.divFin]
-            divFin_buffers = tuple(data[ddivFin.compute_slices] for data in ddivFin.buffers)
         else:
             ddivFin = None
-            divFin_buffers = [None]
         
         if self.compute_divFout:
             ddivFout  = self.output_discrete_fields[self.divFout]
-            divFout_buffers = tuple(data[ddivFout.compute_slices] for data in ddivFout.buffers)
         else:
             ddivFout = None
-            divFout_buffers = [None]
         
-        dim, dtype, ctype = self.dim, self.dtype, self.ctype
-        resolution = dFin.compute_resolution
-        length     = dFin.domain.length
-        axes       = npw.arange(dim)[::-1]
-        for b in (Fin_buffers + Fout_buffers + divFin_buffers + divFout_buffers):
-            if (b is None):
-                continue
-            assert b.dtype == dtype, b.dtype
-            assert npw.array_equal(npw.argsort(b.strides), axes)
+        kd1s, kd2s = self.kd1s, self.kd2s
+        dkd1s = ()
+        for kd1 in kd1s:
+            dkd1 = [None,]*len(kd1)
+            for wi in kd1:
+                idx, dwi, _ = self.tg.discrete_wave_numbers[wi]
+                dkd1[idx] = dwi
+            dkd1s += (tuple(dkd1),)
         
-        self.dFin  = dFin
-        self.dFout = dFout
-        self.Fin_buffers  = Fin_buffers
-        self.Fout_buffers = Fout_buffers
-
+        dkd2s = ()
+        for kd2 in kd2s:
+            dkd2 = [None,]*len(kd1)
+            for wi in kd2:
+                idx, dwi, _ = self.tg.discrete_wave_numbers[wi]
+                dkd2[idx] = dwi
+            dkd2s += (tuple(dkd2),)
+        
+        self.dFin     = dFin
+        self.dFout    = dFout
         self.ddivFin  = ddivFin
         self.ddivFout = ddivFout
-        self.divFin_buffers  = divFin_buffers
-        self.divFout_buffers = divFout_buffers
+        self.dkd1s = tuple(dkd1s)
+        self.dkd2s = tuple(dkd2s)
+
+    
+    def get_work_properties(self):
+        requests = super(SolenoidalProjectionOperatorBase, self).get_work_properties()
+        for (i,(Ft,Bt)) in enumerate(zip(self.forward_transforms, 
+                                         self.backward_transforms)):
+            assert (Ft.backend == Bt.backend)
+            assert (Ft.output_dtype == Bt.input_dtype), (Ft.output_dtype, Bt.input_dtype)
+            assert (Ft.output_shape == Bt.input_shape), (Ft.output_shape, Bt.input_shape)
+            shape = Ft.output_shape
+            dtype = Ft.output_dtype
+            request = MemoryRequest(backend=self.tg.backend, dtype=dtype, 
+                                    shape=shape, nb_components=1,
+                                    alignment=self.min_fft_alignment)
+            requests.push_mem_request('fft_buffer_{}'.format(i), request)
+        return requests
+
+    def setup(self, work):
+        dkd1s, dkd2s = self.dkd1s, self.dkd2s
         
-        self.axes  = axes
-        self.resolution = resolution
-        self.backend = dFin.backend
+        output_axes = self.forward_transforms[0].output_axes
+        for (i,(Ft,Bt)) in enumerate(zip(self.forward_transforms, self.backward_transforms)):
+            dtmp, = work.get_buffer(self, 'fft_buffer_{}'.format(i))
+            Ft.configure_output_buffer(dtmp)
+            Bt.configure_input_buffer(dtmp)
+            assert output_axes == Ft.output_axes
+            assert output_axes == Bt.input_axes
+
+        super(SolenoidalProjectionOperatorBase, self).setup(work)
+
+        reorder_fields = tuple(2-i for i in output_axes)
+        self.FIN  = tuple(self.forward_transforms[i].full_output_buffer for i in reorder_fields)
+        self.FOUT = tuple(self.backward_transforms[i].full_input_buffer for i in reorder_fields)
+
+        self.K  = sum((dkd1s[i] for i in reorder_fields), ())
+        self.KK = sum((dkd2s[i] for i in reorder_fields), ())
+
+        if self.compute_divFin:
+            self.DIV_IN = (self.backward_divFin_transform.full_input_buffer,)
+        else:
+            self.DIV_IN = None
+
+        if self.compute_divFout:
+            self.DIV_OUT = (self.backward_divFout_transform.full_input_buffer,)
+        else:
+            self.DIV_OUT = None
 
-        self.generate_wave_numbers(dim, resolution, length, dtype, ctype, axes)
 
-    @abstractmethod
-    def generate_wave_numbers(self, dim, resolution, length, dtype, ctype, axes):
-        pass
diff --git a/hysop/operator/base/spectral_operator.py b/hysop/operator/base/spectral_operator.py
new file mode 100644
index 0000000000000000000000000000000000000000..70038c312825e73c805ff91ae2a3f76ef11a838d
--- /dev/null
+++ b/hysop/operator/base/spectral_operator.py
@@ -0,0 +1,1809 @@
+
+import warnings
+import sympy as sm
+import numpy as np
+
+from hysop.constants         import BoundaryCondition, BoundaryExtension, TransformType, \
+                                    MemoryOrdering, TranspositionState, Backend, \
+                                    SpectralTransformAction, Implementation
+from hysop.tools.misc        import compute_nbytes
+from hysop.tools.types       import check_instance, to_tuple, first_not_None
+from hysop.tools.decorators  import debug
+from hysop.tools.units       import bytes2str
+from hysop.tools.numerics import is_fp, is_complex, complex_to_float_dtype, \
+                                 float_to_complex_dtype, determine_fp_types
+from hysop.tools.spectral_utils import SpectralTransformUtils as STU
+from hysop.core.arrays.array_backend import ArrayBackend
+from hysop.core.arrays.array import Array
+from hysop.core.memory.memory_request import MemoryRequest, OperatorMemoryRequests
+from hysop.core.graph.graph import not_initialized as _not_initialized, \
+                                   initialized     as _initialized,     \
+                                   discretized     as _discretized,     \
+                                   ready           as _ready
+from hysop.core.graph.computational_node_frontend import ComputationalGraphNodeFrontend
+from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
+from hysop.fields.continuous_field import Field, ScalarField, TensorField
+from hysop.symbolic.array import SymbolicArray
+from hysop.symbolic.spectral import WaveNumber, SpectralTransform, AppliedSpectralTransform
+from hysop.numerics.fft.fft import FFTI, simd_alignment, is_byte_aligned, HysopFFTWarning
+
+class SpectralComputationalGraphNodeFrontend(ComputationalGraphNodeFrontend):
+        
+    def __init__(self, implementation, **kwds):
+        impl, extra_kwds = self.get_actual_implementation(implementation=implementation, **kwds)
+        for k in extra_kwds.keys():
+            assert k not in kwds
+        kwds.update(extra_kwds)
+        super(SpectralComputationalGraphNodeFrontend, self).__init__(
+                implementation=impl, **kwds)
+
+    
+    @classmethod
+    def get_actual_implementation(cls, implementation, 
+            enforce_implementation=True, cl_env=None,
+            **kwds):
+        """
+        Parameters
+        ----------
+        implementation: Implementation, optional, defaults to None
+            User desired target implementation.
+        enforce_implementation: bool, optional, defaults to True
+            If this is set to True, input implementation is enforced.
+            Else, this function may select another implementation when some conditions are met:
+                Case 1: Host FFT by mapping CPU OpenCL buffers
+                   Conditions:
+                     a/ input implementation is set to OPENCL
+                     b/ cl_env.device is of type CPU
+                     c/ Implementation.PYTHON is a valid operator implementation
+                     d/ Target python operator supports OPENCL as backend
+                     e/ OpenCL platform has zero copy capabilities (cannot be checked)
+                   => If cl_env is not given, this will yield a RuntimeError
+                   => In this case PYTHON implementation is chosen instead.
+                      Buffer are mapped to host.
+                      By default this should give multithread FFTW + multithreaded numba.
+                For all other cases, this parameter is ignored.
+
+        Notes
+        -----
+        clFFT (gpyFFT) support for OpenCL CPU devices is a bit neglected.
+        This function allows to override the implementation target from 
+        OPENCL to PYTHON when a CPU OpenCL environment is given as input.
+
+        By default, the CPU FFT target is FFTW (pyFFTW) which has much 
+        better support (multithreaded fftw + multithreaded numba).
+        
+        OpenCL buffers are mapped to host memory with enqueue_map_buffer 
+        (this makes the assumption thal all OpenCL buffers have been allocated 
+        with zero-copy capability in the target OpenCL platform).
+        """
+        implementation = first_not_None(implementation, cls.default_implementation())
+        assert implementation in cls.implementations()
+        extra_kwds = { 'enable_opencl_host_buffer_mapping': False }
+        if (enforce_implementation):
+            return (implementation, extra_kwds)
+        if (implementation == Implementation.OPENCL):
+            if (cl_env is None):
+                msg='enforce_implementation was set to False, '
+                msg+='implementation is OPENCL, but no cl_env was passed '
+                msg+='to check if the device is of type CPU.'
+                raise RuntimeError(msg)
+            from hysop.backend.device.opencl import cl
+            if (cl_env.device.type == cl.device_type.CPU):
+                extra_kwds['enable_opencl_host_buffer_mapping'] = True
+                if (Implementation.PYTHON in cls.implementations()):
+                    from hysop.backend.host.host_operator import HostOperator, OpenClMappable
+                    op_cls = cls.implementations()[Implementation.PYTHON]
+                    if not issubclass(op_cls, HostOperator):
+                        msg='Operator {} is not a HostOperator.'
+                        msg=msg.format(op_cls)
+                        raise TypeError(msg)
+                    if not issubclass(op_cls, OpenClMappable):
+                        msg='Operator {} does not support host to device opencl buffer mapping.'
+                        msg=msg.format(op_cls)
+                        raise TypeError(msg)
+                    assert Backend.HOST   in op_cls.supported_backends()
+                    assert Backend.OPENCL in op_cls.supported_backends()
+                    return (Implementation.PYTHON, extra_kwds)
+        return (implementation, extra_kwds)
+
+
+
+class SpectralOperatorBase(object):
+    """
+    Common implementation interface for spectral based operators.
+    """
+    
+    min_fft_alignment = simd_alignment #FFTW SIMD.
+    
+    @debug
+    def __init__(self, fft_interface=None, fft_interface_kwds=None, 
+                        **kwds):
+        """
+        Initialize a spectral operator base.
+        kwds: dict
+            Base class keyword arguments.
+        """
+        super(SpectralOperatorBase, self).__init__(**kwds)
+
+        check_instance(fft_interface, FFTI, allow_none=True)
+        check_instance(fft_interface_kwds, dict, allow_none=True)
+
+        self.transform_groups = {} # dict[tag] -> SpectralTransformGroup
+        
+        # those values will be deleted at discretization
+        self._fft_interface = fft_interface
+        self._fft_interface_kwds = fft_interface_kwds
+
+    @property
+    def backend(self):
+        msg='FFT array backend depends on the transform group. Please use op.transform_group[key].backend instead.'
+        raise AttributeError(msg)
+
+    @property
+    def FFTI(self):
+        msg='FFT interface depends on the transform group. Please use op.transform_group[key].FFTI instead.'
+        raise AttributeError(msg)
+
+    def new_transform_group(self, tag=None, mem_tag=None):
+        """
+        Register a new SpectralTransformGroup to this spectral operator.
+        A SpectralTransformGroup is an object that collect forward and
+        backward field transforms as well as symbolic expressions and
+        wave_numbers symbols.
+        """
+        n    = len(self.transform_groups)
+        tag  = first_not_None(tag, 'transform_group_{}'.format(n))
+        msg  = 'Tag "{}" has already been registered.'
+        assert (tag not in self.transform_groups), msg.format(tag)
+        trg = SpectralTransformGroup(op=self, tag=tag, mem_tag=mem_tag)
+        self.transform_groups[tag] = trg
+        return trg
+
+    def initialize(self, **kwds):
+        super(SpectralOperatorBase, self).initialize(**kwds)
+        for tg in self.transform_groups.values():
+            backend = tg.initialize(**kwds)
+    
+    
+    def get_field_requirements(self):
+        requirements = super(SpectralOperatorBase, self).get_field_requirements()
+        
+        for is_input, (field, td, req) in requirements.iter_requirements():
+            req.memory_order = MemoryOrdering.C_CONTIGUOUS
+            req.axes = (TranspositionState[field.dim].default_axes(),)
+            can_split = req.can_split
+            can_split[-1]  = False
+            can_split[:-1] = True
+            req.can_split = can_split
+        return requirements
+    
+    @debug
+    def get_node_requirements(self):
+        node_reqs = super(SpectralOperatorBase, self).get_node_requirements()
+        node_reqs.enforce_unique_topology_shape = True
+        return node_reqs
+    
+    
+    def discretize(self, **kwds):
+        super(SpectralOperatorBase, self).discretize(**kwds)
+        
+        for tg in self.transform_groups.values():
+            tg.discretize(fft_interface=self._fft_interface,
+                          fft_interface_kwds=self._fft_interface_kwds,
+                          enable_opencl_host_buffer_mapping=self.enable_opencl_host_buffer_mapping,
+                          **kwds)
+        del self._fft_interface
+        del self._fft_interface_kwds
+    
+    def get_mem_requests(self, **kwds):
+        memory_requests = {}
+        for tg in self.transform_groups.values():
+            for (k,v) in tg.get_mem_requests(**kwds).iteritems():
+                check_instance(k, str) # temporary buffer name
+                check_instance(v, int)    # nbytes
+                K = (k,tg.backend)
+                if K in memory_requests:
+                    memory_requests[K] = max(memory_requests[K], v)
+                else:
+                    memory_requests[K] = v
+        return memory_requests
+
+    def get_work_properties(self, **kwds):
+        requests = super(SpectralOperatorBase, self).get_work_properties(**kwds)
+        for ((k,backend),v) in self.get_mem_requests(**kwds).iteritems():
+            check_instance(k, str)
+            check_instance(v, (int, long))
+            if (v>0):
+                mrequest = MemoryRequest(backend=backend, size=v, 
+                                            alignment=self.min_fft_alignment) 
+                requests.push_mem_request(request_identifier=k, mem_request=mrequest)
+        return requests
+
+
+    def setup(self, work):
+        self.allocate_tmp_fields(work)
+        for tg in self.transform_groups.values():
+            tg.setup(work=work)
+        super(SpectralOperatorBase, self).setup(work=work)
+
+
+
+class SpectralTransformGroup(object):
+    """
+    Build and check a FFT transform group.    
+    
+    This object tells the planner to build a full forward transform for all given 
+    forward_fields. The planner will also build backward transforms for all specified 
+    backward_fields.
+        
+    The object will also automatically build per-axis wavenumbers up to certain powers,
+    extracted from user provided sympy expressions.
+
+    Finally boundary condition (ie. transform type) compability will be checked by 
+    using user provided sympy expressions.
+        
+    Calling a forward transform ensures that forward source field is read-only
+    and not destroyed.
+    """
+    DEBUG=False
+
+    def __init__(self, op, tag, mem_tag, **kwds):
+        """
+        Parameters
+        ----------
+        op : SpectralOperatorBase
+            Operator that creates this SpectralTransformGroup.
+        tag: str
+            A tag to identify this transform group.
+            Each tag can only be registered once in a SpectralOperatorBase instance.
+        
+        Attributes:
+        -----------
+        tag: str
+            SpectralTransformGroup identifier.
+        mem_tag: str
+            SpectralTransformGroup memory pool identifier.
+        forward_transforms: list of forward SpectralTransform
+            Forward fields to be planned for transform, according to Field boundary conditions.
+        backward_fields: list of backward SpectralTransform
+            Backward fields to be planned for transform, according to Field boundary conditions.
+        
+        Notes
+        -----
+        All forward_fields and backward_fields have to live on the same domain and
+        their boundary conditions should comply with given expressions.
+        """
+        mem_tag = first_not_None(mem_tag, 'fft_pool')
+        check_instance(op, SpectralOperatorBase)
+        check_instance(tag, str)
+        check_instance(mem_tag, str)
+
+        self._op = op
+        self._tag = tag
+        self._mem_tag = mem_tag
+
+        self._forward_transforms = {}
+        self._backward_transforms = {}
+
+        self._wave_numbers = set()
+        self._indexed_wave_numbers = {}
+        self._expressions = ()
+        
+        self._discrete_wave_numbers = None
+
+    def indexed_wavenumbers(self, *wave_numbers):
+        return tuple(self._indexed_wave_numbers[Wi] for Wi in wave_numbers)
+    
+    @property
+    def op(self):
+        return self._op
+    @property
+    def tag(self):
+        return self._tag
+    @property
+    def mem_tag(self):
+        return self._mem_tag
+    @property
+    def name(self):
+        return self._tag
+
+    @property
+    def initialized(self):
+        return self._op.initialized
+    @property
+    def discretized(self):
+        return self._op.discretized
+    @property
+    def ready(self):
+        return self._op.ready
+
+    @property
+    def forward_fields(self):
+        return map(lambda x: x[0], self._forward_transforms.keys())
+    @property
+    def backward_fields(self):
+        return map(lambda x: x[0], self._backward_transforms.keys())
+    @property
+    def forward_transforms(self):
+        return self._forward_transforms
+    @property
+    def backward_transforms(self):
+        return self._backward_transforms
+    
+    @_not_initialized
+    def initialize(self, 
+                    fft_granularity=None, 
+                    fft_concurrent_plans=1, 
+                    fft_plan_workload=1,
+                    **kwds):
+        """
+        Should be called after all require_forward_transform and require_backward_transform
+        calls.
+        
+        Parameters
+        ----------
+        fft_granularity: int, optional
+            Granularity of each directional fft plan.
+              1:   iterate over 1d lines   (slices of dimension 1)
+              2:   iterate over 2d planes  (slices of dimension 2)
+              3:   iterate over 3d blocks  (slices of dimension 3)
+              n-1: iterate over hyperplans (slices of dimension n-1)
+              n :  no iteration, the plan will handle the whole domain.
+            Contiguous buffers with sufficient alignement are allocated. 
+            Default value is: 1 in 1D else n-1 (ie. hyperplans)
+        fft_plan_workload: int, optional, defaults to 1
+            The number of blocks of dimension fft_granularity that a 
+            single plan will handle at once. Default is one block.
+        fft_concurrent_plans: int, optional, defaults to 1
+            Number of concurrent plans. 
+            Should be 1 for HOST based FFT interfaces.
+            Should be at least 3 for DEVICE based FFT interface if the device
+            has two async copy engine (copy, transform, copy).
+        """
+        (domain, dim) = self.check_fields(self.forward_fields, self.backward_fields) 
+
+        fft_granularity = first_not_None(fft_granularity, max(1,dim-1))
+        check_instance(fft_granularity, int, minval=1, maxval=dim)
+        check_instance(fft_concurrent_plans, int, minval=1)
+        check_instance(fft_plan_workload, int, minval=1)
+
+        self._fft_granularity      = fft_granularity
+        self._fft_concurrent_plans = fft_concurrent_plans
+        self._fft_plan_workload    = fft_plan_workload
+
+        self._domain = domain
+        self._dim = dim
+    
+    @_initialized
+    def discretize(self, fft_interface, fft_interface_kwds, 
+            enable_opencl_host_buffer_mapping, **kwds):
+        backends = set()
+        grid_resolutions = set()
+        compute_axes   = set()
+        compute_shapes = set()
+        compute_dtypes = set()
+        for fwd in self.forward_transforms.values():
+            fwd.discretize()
+            backends.add(fwd.backend)
+            grid_resolutions.add(to_tuple(fwd.dfield.mesh.grid_resolution))
+            compute_axes.add(fwd.output_axes)
+            compute_shapes.add(fwd.output_shape)
+            compute_dtypes.add(fwd.output_dtype)
+        for bwd in self.backward_transforms.values():
+            bwd.discretize()
+            backends.add(bwd.backend)
+            grid_resolutions.add(to_tuple(bwd.dfield.mesh.grid_resolution))
+            compute_axes.add(bwd.input_axes)
+            compute_shapes.add(bwd.input_shape)
+            compute_dtypes.add(bwd.input_dtype)
+
+        def format_error(data):
+            return '\n  *'+ '\n  *'.join(str(x) for x in data)
+        msg='Fields do not live on the same backend:'+format_error(backends)
+        assert len(backends)==1, msg
+        msg='Fields grid size mismatch:'+format_error(grid_resolutions)
+        assert len(grid_resolutions)==1, msg
+        assert len(compute_axes)==1,   'Fields axes mismatch:'+format_error(compute_axes)
+        assert len(compute_shapes)==1, 'Fields shape mismatch:'+format_error(compute_shapes)
+        assert len(compute_dtypes)==1, 'Fields data type mismatch.'+format_error(compute_dtypes)
+
+        backend         = next(iter(backends))
+        grid_resolution = next(iter(grid_resolutions))
+        compute_axes    = next(iter(compute_axes))
+        compute_shape   = next(iter(compute_shapes))
+        compute_dtype   = next(iter(compute_dtypes))
+        
+        if enable_opencl_host_buffer_mapping:
+            msg='Trying to enable opencl device to host buffer mapping on {} target.'
+            assert (backend.kind is Backend.OPENCL), msg.format(backend.kind)
+
+        if (fft_interface is None):
+            fft_interface_kwds = first_not_None(fft_interface_kwds, {})
+            fft_interface = FFTI.default_interface_from_backend(backend, 
+                    enable_opencl_host_buffer_mapping=enable_opencl_host_buffer_mapping,
+                    **fft_interface_kwds)
+        else:
+            assert not interface_kwds, 'FFT interface has already been built.'
+        
+        check_instance(fft_interface, FFTI)
+        fft_interface.check_backend(backend, 
+                enable_opencl_host_buffer_mapping=enable_opencl_host_buffer_mapping)
+
+        buffer_backend = backend
+        host_backend   = backend.host_array_backend
+        backend        = fft_interface.backend
+        
+        discrete_wave_numbers = {}
+        for wn in self._wave_numbers:
+            (idx, freqs, nd_freqs) = self.build_wave_number(self._domain, grid_resolution,
+                                                          backend, wn,
+                                                          compute_dtype, compute_axes, compute_shape)
+            self._indexed_wave_numbers[wn].indexed_object.to_backend(backend.kind).bind_memory_object(freqs)
+            self._indexed_wave_numbers[wn].index.bind_axes(compute_axes)
+            discrete_wave_numbers[wn] = (idx, freqs, nd_freqs)
+        self._discrete_wave_numbers = discrete_wave_numbers
+
+        self.buffer_backend = buffer_backend
+        self.host_backend   = host_backend
+        self.backend        = backend
+        self.FFTI           = fft_interface
+        
+        self.grid_resolution = grid_resolution
+        self.compute_axes  = compute_axes
+        self.compute_shape = compute_shape
+        self.compute_dtype = compute_dtype
+       
+    @classmethod
+    def build_wave_number(cls, domain, grid_resolution,
+                            backend, wave_number, 
+                            compute_dtype, compute_axes, 
+                            compute_resolution):
+
+        dim    = domain.dim
+        length = domain.length
+
+        ftype, ctype = determine_fp_types(compute_dtype)
+        
+        axis      = wave_number.axis
+        transform = wave_number.transform
+        exponent  = wave_number.exponent
+        
+        idx = compute_axes.index(axis)
+
+        L = domain.length[axis]
+        N = grid_resolution[axis]
+        
+        freqs = STU.compute_wave_numbers(transform=transform, N=N, L=L, ftype=ftype)
+        freqs = freqs**exponent
+        if STU.is_R2R(transform):
+            sign_offset = STU.is_cosine(transform)
+            freqs *= (-1)**((exponent+sign_offset)//2)
+        
+        assert exponent != 0, 'exponent cannot be zero.'
+        assert exponent > 0, 'negative powers not implemented yet.'
+        if is_complex(freqs.dtype) and (exponent % 2 == 0):
+            assert freqs.imag.sum() == 0
+            freqs = freqs.real.copy()
+
+        backend_freqs = backend.empty_like(freqs)
+        backend_freqs[...] = freqs
+        freqs = backend_freqs
+
+        nd_shape = [1,]*dim
+        nd_shape[idx] = freqs.size
+        nd_shape = tuple(nd_shape)
+        nd_freqs = freqs.reshape(nd_shape)
+
+        if cls.DEBUG:
+            print
+            print 'BUILD WAVENUMBER' 
+            print 'backend:       {}'.format(backend.kind)
+            print 'grid_shape:    {}'.format(grid_resolution)
+            print 'length:        {}'.format(length)
+            print '-----'
+            print 'ftype:         {}'.format(ftype)
+            print 'ctype:         {}'.format(ctype)
+            print 'compute shape: {}'.format(compute_resolution)
+            print 'compute axes:  {}'.format(compute_axes)
+            print '-----'
+            print 'wave_number:'
+            print '  *symbolic:   {}'.format(wave_number)
+            print '  *axis:       {}'.format(axis)
+            print '  *transform:  {}'.format(transform)
+            print '  *exponent:   {}'.format(exponent)
+            print '----'
+            print 'L:             {}'.format(L)
+            print 'N:             {}'.format(N)
+            print 'freqs:         {}'.format(freqs)
+            print 'nd_freqs:      {}'.format(nd_freqs)
+            print '----'
+
+        return (idx, freqs, nd_freqs)
+    
+    @_discretized
+    def get_mem_requests(self, **kwds):
+        memory_requests = {}
+        for fwd in self.forward_transforms.values():
+            mem_requests = fwd.get_mem_requests(**kwds)
+            check_instance(mem_requests, dict, keys=str, values=(int,long))
+            for (k,v) in mem_requests.iteritems():
+                if k in memory_requests:
+                    memory_requests[k] = max(memory_requests[k], v)
+                else:
+                    memory_requests[k] = v
+        for bwd in self.backward_transforms.values():
+            mem_requests = bwd.get_mem_requests(**kwds)
+            check_instance(mem_requests, dict, keys=str, values=(int,long))
+            for (k,v) in mem_requests.iteritems():
+                if k in memory_requests:
+                    memory_requests[k] = max(memory_requests[k], v)
+                else:
+                    memory_requests[k] = v
+        return memory_requests
+    
+    @_discretized
+    def setup(self, work):
+        for fwd in self.forward_transforms.values():
+            fwd.setup(work=work)            
+        for bwd in self.backward_transforms.values():
+            bwd.setup(work=work)            
+
+    @_not_initialized
+    def require_forward_transform(self, field, axes=None, transform_tag=None,
+                                    custom_output_buffer=None, action=None):
+        """
+        Tells this SpectralTransformGroup to build a forward SpectralTransform
+        on given field. Only specified axes are transformed.
+
+        Boundary condition to FFT extension mapping:
+            Periodic:               Periodic extension
+            Homogeneous Dirichlet:  Odd extension
+            Homogeneous Neumann:    Even extension
+        
+        This leads to 5 possible transforms for each axis (periodic-periodic, even-even, 
+        odd-odd, even-odd, odd-even).
+
+        Forward transforms used for each axis per extension pair:
+            *Periodic-Periodic   (PER-PER):   DFT (C2C, R2C for the first periodic axis)
+            *Dirichlet-Dirichlet (ODD-ODD):   DST-I
+            *Dirichlet-Neumann   (ODD-EVEN):  DST-III
+            *Neumann-Dirichlet   (EVEN-ODD):  DCT-III
+            *Neumann-Neumann     (EVEN-EVEN): DCT-I
+
+        This method will return the SpectralTransform object associated to field.
+        
+        Parameters
+        ----------
+        field: ScalarField 
+            The source field to be transformed.
+        axes: array-like of integers
+            The axes to be transformed.
+        transform_tag: str
+            Extra tag to register the forward transform (a single scalar field can be 
+            transformed multiple times). Default tag is 'default'.
+        custom_output_buffer: None or str, optional
+            Force this transform to output in one of the two common transform group buffers.
+            Default None value will force the user allocate an output buffer.
+            Specifying 'B0' or 'B1' will tell the planner to output the transform
+            in one of the two transform group buffers (that are used during all forward
+            and backward transforms of the same transform group). This features allow
+            FFT operators to save one buffer for the last forward transform.
+            Specifying 'auto' will tell the planner to choose either 'B0' or 'B1'.
+        action: BackwardTransfromAction, optional
+            Defaults to SpectralTransformAction.OVERWRITE which will overwrite the 
+            compute slices of the output buffer.
+            SpectralTransformAction.ACCUMULATE will sum the current content of the buffer
+            with the result of the forward transform.
+        """
+        transform_tag = first_not_None(transform_tag, 'default')
+        action = first_not_None(action, SpectralTransformAction.OVERWRITE)
+        transforms = SpectralTransform(field=field, axes=axes, forward=False)
+        check_instance(field, Field)
+        check_instance(transform_tag, str)
+        check_instance(action, SpectralTransformAction)
+        transforms = SpectralTransform(field=field, axes=axes, forward=True)
+        msg='Field {} with axes {} and transform_tag "{}" has already been registered for forward transform.'
+        if field.is_tensor:
+            planned_transforms = field.new_empty_array()
+            for (idx, f) in field.nd_iter():
+                assert (f,axes,transform_tag) not in self._forward_transforms, msg.format(f.name, axes, transform_tag)
+                assert f in self._op.input_fields
+                assert f is transforms[idx].field
+                assert transforms[idx].is_forward
+                planned_transforms[idx] = PlannedSpectralTransform(transform_group=self,
+                                                    tag=self.tag + '_' + transform_tag + '_' + f.name,
+                                                    symbolic_transform=transforms[idx],
+                                                    custom_output_buffer=custom_output_buffer, 
+                                                    action=action)
+                self._forward_transforms[(f,axes,transform_tag)] = planned_transforms[idx]
+        else:
+            assert (field,axes,transform_tag) not in self._forward_transforms, msg.format(field.name, axes, transform_tag)
+            assert field in self._op.input_fields
+            assert field is transforms.field
+            assert transforms.is_forward
+            planned_transforms = PlannedSpectralTransform(transform_group=self,
+                                                    tag=self.tag + '_' + transform_tag + '_' + field.name,
+                                                    symbolic_transform=transforms,
+                                                    custom_output_buffer=custom_output_buffer,
+                                                    action=action)
+            self._forward_transforms[(field,axes,transform_tag)] = planned_transforms
+        return planned_transforms
+    
+    @_not_initialized
+    def require_backward_transform(self, field, axes=None, transform_tag=None,
+                                        custom_input_buffer=None,
+                                        matching_forward_transform=None,
+                                        action=None):
+        """
+        Same as require_forward_transform but for backward transforms.
+        This corresponds to the following backward transform mappings:
+        
+        if order[axis] is 0:
+            *no transform -> no transform
+        else, if order[axis] is even:
+            *C2C     -> C2C
+            *R2C     -> C2R
+            *DCT-I   -> DCT-I
+            *DCT-III -> DCT-II
+            *DST-I   -> DST-I
+            *DST-III -> DST-II
+        else: (if order[axis] is odd)
+            *C2C     -> C2C
+            *R2C     -> C2R
+            *DCT-I   -> DST-I
+            *DCT-III -> DST-II
+            *DST-I   -> DCT-I
+            *DST-III -> DCT-II
+        
+        For backward transforms, boundary compatibility for output_fields is thus the following:
+            if axis is even:
+                Boundary should be exactly the same on the axis.
+            else, if axis is odd, boundary conditions change on this axe:
+                *(Periodic-Peridic)      PER-PER -> PER-PER   (Periodic-Periodic)
+                *(Neumann-Neumann)     EVEN-EVEN -> ODD-ODD   (Dirichlet-Dirichlet)
+                *(Neumann-Dirichlet)    EVEN-ODD -> ODD-EVEN  (Dirichlet-Neumann)
+                *(Dirichlet-Neumann)    ODD-EVEN -> EVEN-ODD  (Neumman-Dirichlet)
+                *(Dirichlet-Dirichlet)   ODD-ODD -> EVEN-EVEN (Neumann-Neumann)
+
+        Order and boundary conditions are decuded from field.
+        
+        Parameters
+        ----------
+        field: ScalarField 
+            The target field where the result of the inverse transform will be stored.
+        axes: array-like of integers
+            The axes to be transformed.
+        transform_tag: str
+            Extra tag to register the backward transform (a single scalar field can be 
+            transformed multiple times). Default tag is 'default'.
+        custom_input_buffer: None or str or F, optional
+            Force this transform to take as input one of the two common transform group buffers.
+            Default None value will force the user to supply an input buffer.
+            Specifying 'B0' or 'B1' will tell the planner to take as transform input
+            one of the two transform group buffers (that are used during all forward
+            and backward transforms of the same transform group). This features allow
+            FFT operators to save one buffer for the first backward transform.
+            Specifying 'auto' will tell the planner to use the matching
+            transform output buffer.
+        action: BackwardTransfromAction, optional
+            Defaults to SpectralTransformAction.OVERWRITE which will overwrite the 
+            compute slices of the given output field.
+            SpectralTransformAction.ACCUMULATE will sum the current content of the field
+            with the result of the backward transform.
+            
+        """
+        transform_tag = first_not_None(transform_tag, 'default')
+        action = first_not_None(action, SpectralTransformAction.OVERWRITE)
+        check_instance(field, Field)
+        check_instance(transform_tag, str)
+        check_instance(action, SpectralTransformAction)
+        transforms = SpectralTransform(field=field, axes=axes, forward=False)
+        msg='Field {} with axes {} and transform_tag "{}" has already been registered for backward transform.'
+        if field.is_tensor:
+            planned_transforms = field.new_empty_array()
+            for (idx, f) in field.nd_iter():
+                assert (f,axes,transform_tag) not in self._backward_transforms, msg.format(f.name, axes, transform_tag)
+                assert f in self._op.output_fields
+                assert not transforms[idx].is_forward
+                planned_transforms[idx] = PlannedSpectralTransform(transform_group=self,
+                                                    tag=self.tag + '_' + transform_tag + '_' + f.name,
+                                                    symbolic_transform=transforms[idx],
+                                                    custom_input_buffer=custom_input_buffer,
+                                                    matching_forward_transform=matching_forward_transform,
+                                                    action=action)
+                self._backward_transforms[(f,axes,transform_tag)] = planned_transforms[idx]
+        else:
+            assert (field,axes,transform_tag) not in self._backward_transforms, msg.format(field.name, axes, transform_tag)
+            assert field in self._op.output_fields
+            assert not transforms.is_forward
+            planned_transforms = PlannedSpectralTransform(transform_group=self,
+                                                    tag=self.tag + '_' + transform_tag + '_' + field.name,
+                                                    symbolic_transform=transforms,
+                                                    custom_input_buffer=custom_input_buffer,
+                                                    matching_forward_transform=matching_forward_transform,
+                                                    action=action)
+            self._backward_transforms[(field,axes,transform_tag)] = planned_transforms
+        return planned_transforms
+
+    @property
+    def discrete_wave_numbers(self):
+        assert self.discretized
+        discrete_wave_numbers = self._discrete_wave_numbers
+        if (discrete_wave_numbers is None):
+            msg='discrete_wave_numbers has not been set yet.'
+            raise AttributeError(msg)
+        return self._discrete_wave_numbers
+    
+    @_not_initialized
+    def push_expressions(self, *exprs):
+        exprs_wave_numbers = set()
+        for expr in exprs:
+            assert isinstance(expr, sm.Basic)
+            (e, transforms, wn) = STU.parse_expression(expr, replace_pows=True)
+            self._expressions += (e,)
+            self._wave_numbers.update(wn)
+            for _wn in wn:
+                if (_wn not in self._indexed_wave_numbers):
+                    self._indexed_wave_numbers[_wn] = _wn.indexed_buffer()
+            exprs_wave_numbers.update(wn)
+            if (self.DEBUG):
+                print '\n\nPARSING EXPRESSION {}'.format(expr)
+                print '  new_expr:     {}'.format(e)
+                print '  transforms:   {}'.format(transforms)
+                print '  wave_numbers: {}'.format(wn)
+
+        return tuple(exprs_wave_numbers)
+            
+        
+    @classmethod
+    def check_fields(cls, forward_fields, backward_fields):
+        all_fields = tuple(set(forward_fields+backward_fields))
+        if not all_fields:
+            msg='At least one field is required.'
+            raise ValueError(msg)
+        domain = cls.determine_domain(*all_fields)
+        dim    = domain.dim
+        return (domain, dim)
+
+    @classmethod 
+    def determine_domain(cls, *fields):
+        domain = fields[0].domain
+        for field in fields[1:]:
+            if (field.domain is not domain):
+                msg='Domain mismatch between fields:\n{}\nvs.\n{}\n'
+                msg=msg.format(domain, field.domain)
+                raise ValueError(msg)
+        return domain
+
+
+
+class PlannedSpectralTransform(object):
+    """
+    A planned spectral transform is an AppliedSpectralTransform wrapper.
+    This object will be handled by the transform planner.
+    """
+    DEBUG=False
+
+    def __init__(self, transform_group, tag, symbolic_transform, action,
+            custom_input_buffer=None, custom_output_buffer=None,
+            matching_forward_transform=None):
+        
+        check_instance(transform_group, SpectralTransformGroup)
+        check_instance(transform_group.op, SpectralOperatorBase)
+        check_instance(tag, str)
+        check_instance(symbolic_transform, AppliedSpectralTransform)
+        check_instance(action, SpectralTransformAction)
+        assert custom_input_buffer  in (None, 'B0', 'B1', 'auto'), custom_input_buffer
+        assert custom_output_buffer in (None, 'B0', 'B1', 'auto'), custom_output_buffer
+
+        self._transform_group = transform_group
+        self._tag = tag
+        self._symbol = symbolic_transform
+        self._queue = None
+        self._custom_input_buffer  = custom_input_buffer
+        self._custom_output_buffer = custom_output_buffer
+        self._matching_forward_transform = matching_forward_transform
+        self._action = action
+       
+        field      = self.s.field
+        is_forward = self.s.is_forward
+        
+        if is_forward:
+            msg = "Cannot specify 'custom_input_buffer' for a forward transform."
+            assert (custom_input_buffer is None), msg
+            msg = "Cannot specify 'matching_forward_transform' for a forward transform."
+            assert (matching_forward_transform is None), msg
+        else:
+            msg = "Cannot specify 'custom_output_buffer' for a backward transform."
+            assert (self._custom_output_buffer is None), msg
+            if (self._custom_input_buffer == 'auto'):
+                msg="Using 'auto' as 'custom_output_buffer' of a backward transform implies "
+                msg+="to specify a 'matching_forward_transform' to choose the buffer from."
+                assert (matching_forward_transform is not None), msg
+                assert isinstance(matching_forward_transform, PlannedSpectralTransform), msg
+                assert matching_forward_transform.is_forward, msg
+            else:
+                msg="Using 'custom_output_buffer' different than 'auto' for a backward "
+                msg+="transform implies to set 'matching_forward_transform' to None."
+                assert (matching_forward_transform is None), msg
+
+        # reorder transforms in execution order (contiguous axe first)
+        transforms = self.s.transforms[::-1] 
+
+        if len(transforms)!=field.dim:
+            msg='Number of transforms does not match field dimension.'
+            raise ValueError(msg)
+
+        if all((tr is TransformType.NONE) for tr in transforms):
+            msg='All transforms are of type NONE.'
+            raise ValueError(msg)
+        
+        if is_forward:
+            input_dtype  = field.dtype
+            output_dtype = STU.determine_output_dtype(
+                    field.dtype, *transforms)
+        else:
+            input_dtype  = STU.determine_input_dtype(
+                    field.dtype, *transforms)
+            output_dtype = field.dtype
+
+        self._input_dtype  = np.dtype(input_dtype)
+        self._output_dtype = np.dtype(output_dtype)
+
+        self._input_shape  = None
+        self._output_shape = None
+
+        self._input_buffer  = None
+        self._output_buffer = None
+
+        self._dfield = None
+        self._input_symbolic_arrays  = set()
+        self._output_symbolic_arrays = set()
+        self._ready = False
+    
+    def input_symbolic_array(self, name, **kwds):
+        """Create a symbolic array that will be bound to input transform array."""
+        assert ('memory_object' not in kwds)
+        assert ('dim' not in kwds)
+        obj = SymbolicArray(name=name, memory_object=None, 
+                dim=self.field.dim, **kwds)
+        self._input_symbolic_arrays.add(obj)
+        return obj
+    
+    def output_symbolic_array(self, name, **kwds):
+        """Create a symbolic array that will be bound to output transform array."""
+        assert ('memory_object' not in kwds)
+        assert ('dim' not in kwds)
+        obj = SymbolicArray(name=name, memory_object=None, 
+                dim=self.field.dim, **kwds)
+        self._output_symbolic_arrays.add(obj)
+        return obj
+
+    @property
+    def transform_group(self):
+        return self._transform_group
+    @property
+    def op(self):
+        return self._transform_group.op
+
+    @property
+    def tag(self):
+        return self._tag
+    @property
+    def name(self):
+        return self._tag
+
+    @property
+    def symbol(self):
+        return self._symbol
+    @property
+    def s(self):
+        return self._symbol
+    
+    @property
+    def field(self):
+        return self._symbol.field
+    @property
+    def is_forward(self):
+        return self._symbol.is_forward
+    @property
+    def is_backward(self):
+        return not self.is_forward
+    @property
+    def transforms(self):
+        return self._symbol.transforms
+
+    @property
+    def input_dtype(self):
+        return self._input_dtype
+    @property
+    def output_dtype(self):
+        return self._output_dtype
+
+    
+    @property
+    def backend(self):
+        assert self.discretized
+        backend = self._backend
+        if (backend is None):
+            msg='backend has not been set yet.'
+            raise AttributeError(msg)
+        return backend
+    @property
+    def dfield(self):
+        assert self.discretized
+        if (self._dfield is None):
+            msg='dfield has not been set.'
+            raise AttributeError(msg)
+        return self._dfield
+
+    @property
+    def input_shape(self):
+        assert self.discretized
+        if (self._input_shape is None):
+            msg='input_shape has not been set.'
+            raise AttributeError(msg)
+        return self._input_shape
+    @property
+    def output_shape(self):
+        assert self.discretized
+        if (self._output_shape is None):
+            msg='output_shape has not been set.'
+            raise AttributeError(msg)
+        return self._output_shape
+    
+    @property
+    def input_transform_shape(self):
+        assert self.discretized
+        if (self._input_transform_shape is None):
+            msg='input_transform_shape has not been set.'
+            raise AttributeError(msg)
+        return self._input_transform_shape
+    @property
+    def output_transform_shape(self):
+        assert self.discretized
+        if (self._output_transform_shape is None):
+            msg='output_transform_shape has not been set.'
+            raise AttributeError(msg)
+        return self._output_transform_shape
+    
+    @property
+    def input_axes(self):
+        assert self.discretized
+        if (self._input_axes is None):
+            msg='input_axes has not been set.'
+            raise AttributeError(msg)
+        return self._input_axes
+    @property
+    def output_axes(self):
+        assert self.discretized
+        if (self._output_axes is None):
+            msg='output_axes has not been set.'
+            raise AttributeError(msg)
+        return self._output_axes
+    
+    @property
+    def input_slices(self):
+        assert self.discretized
+        buf = self._input_slices
+        if (buf is None):
+            msg='input_slices has not been set yet.'
+            raise AttributeError(msg)
+        return buf
+    @property
+    def output_slices(self):
+        assert self.discretized
+        buf = self._output_slices
+        if (buf is None):
+            msg='output_slices has not been set yet.'
+            raise AttributeError(msg)
+        return buf
+    
+    @property
+    def input_buffer(self):
+        assert self.discretized
+        buf = self._input_buffer
+        if (buf is None):
+            msg='input_buffer has not been set yet.'
+            raise AttributeError(msg)
+        return buf
+    @property
+    def output_buffer(self):
+        assert self.discretized
+        buf = self._output_buffer
+        if (buf is None):
+            msg='output_buffer has not been set yet.'
+            raise AttributeError(msg)
+        return buf
+    
+    @property
+    def full_input_buffer(self):
+        assert self.discretized
+        buf = self._full_input_buffer
+        if (buf is None):
+            msg='full_input_buffer has not been set yet.'
+            raise AttributeError(msg)
+        return buf
+    @property
+    def full_output_buffer(self):
+        assert self.discretized
+        buf = self._full_output_buffer
+        if (buf is None):
+            msg='full_output_buffer has not been set yet.'
+            raise AttributeError(msg)
+        return buf
+    
+    @property
+    def initialized(self):
+        return self.op.initialized
+    @property
+    def discretized(self):
+        return self.op.discretized
+    @property
+    def ready(self):
+        return self._ready
+
+
+    @_not_initialized
+    def initialize(self, **kwds):
+        pass
+    
+    
+    @_initialized
+    def discretize(self, **kwds):
+        is_forward = self.is_forward
+
+        dim = self.field.dim
+        field_axes = TranspositionState[dim].default_axes()
+
+        if is_forward:
+            (dfield, transform_info, transpose_info, transform_offsets) = \
+                    self._discretize_forward(field_axes, **kwds)
+            assert transpose_info[0][1] == field_axes
+        else:
+            (dfield, transform_info, transpose_info, transform_offsets) = \
+                    self._discretize_backward(field_axes, **kwds)
+            assert transpose_info[-1][2] == field_axes
+        assert dfield.dim==len(transform_info)==len(transpose_info)==dim
+        assert transform_info[0][2][1]  == self._input_dtype
+        assert transform_info[-1][3][1] == self._output_dtype
+        
+        # filter out untransformed axes
+        tidx = tuple(filter(lambda i: not STU.is_none(transform_info[i][1]),  xrange(dim)))
+        assert tidx, 'Could not determine any transformed axe.'
+        ntransforms = len(tidx)
+        transform_info = tuple(map(transform_info.__getitem__, tidx))
+        transpose_info = tuple(map(transpose_info.__getitem__, tidx))
+        assert len(transform_info)==len(transpose_info)==ntransforms
+
+        # determine input and output shapes
+        input_axes  = transpose_info[0][1]
+        output_axes = transpose_info[-1][2]
+        if is_forward:
+            assert (field_axes==input_axes), (field_axes, input_axes)
+            input_transform_shape  = transpose_info[0][3]
+            output_transform_shape = transform_info[-1][3][0]
+            input_shape,  input_slices, _  = \
+                    self.determine_buffer_shape(input_transform_shape, False,
+                                                    transform_offsets, input_axes)
+            output_shape, output_slices, zfos = \
+                    self.determine_buffer_shape(output_transform_shape, True,
+                                                    transform_offsets, output_axes)
+            # We have a situation where we should impose zeros:
+            #  1) output transform ghosts (when there are transform sizes mismatch DXT-I variants)
+            zero_fill_output_slices = zfos
+        else:
+            assert (field_axes==output_axes), (field_axes, output_axes)
+            input_transform_shape  = transform_info[0][2][0]
+            output_transform_shape = transpose_info[-1][4]
+
+            input_shape,  input_slices, _  = \
+                    self.determine_buffer_shape(input_transform_shape, True,
+                                                    transform_offsets, input_axes)
+            output_shape, output_slices, zfos = \
+                    self.determine_buffer_shape(output_transform_shape, False,
+                                                    transform_offsets, output_axes)
+            # We have a situation where we should impose zeros:
+            #  1) impose homogeneous dirichlet conditions on output 
+            #      (implicit 0's are not part of the transform output).
+            zero_fill_output_slices = zfos
+
+        self._dfield = dfield
+        self._transform_info = transform_info
+        self._transpose_info = transpose_info
+        self._ntransforms    = ntransforms
+        
+        self._input_axes     = input_axes
+        self._input_shape    = input_shape
+        self._input_slices   = input_slices
+        self._input_transform_shape = input_transform_shape
+
+        self._output_axes    = output_axes
+        self._output_shape   = output_shape
+        self._output_slices  = output_slices
+        self._output_transform_shape = output_transform_shape
+            
+        self._zero_fill_output_slices = zero_fill_output_slices
+
+        self._backend = dfield.backend
+        
+        if self.DEBUG:
+            def axis_format(info):
+                prefix='\n'+' '*4
+                ss=''
+                for (i,data) in enumerate(info):
+                    ss+=prefix+'{}/ '.format(i)+str(data)
+                return ss
+            def slc_format(slices):
+                if (slices is None):
+                    return 'NONE'
+                else:
+                    prefix='\n'+' '*4
+                    ss=''
+                    for slc in slices:
+                        ss+=prefix+str(slc)
+                    return ss
+            print '\n\n== SPECTRAL PLANNING INFO OF FIELD {} =='.format(dfield.pretty_name)
+            print 'transform direction:     {}'.format('FORWARD' if self.is_forward 
+                                                  else 'BACKWARD')
+            print 'transforms:              {}'.format(self.transforms)
+            print ':CARTESIAN INFO:'
+            print 'cart shape:              {}'.format(dfield.topology.cart_shape)
+            print 'global grid resolution:  {}'.format(dfield.mesh.grid_resolution)
+            print 'local  grid resolution:  {}'.format(dfield.compute_resolution)
+            print ':INPUT:'
+            print 'input axes:              {}'.format(self._input_axes)
+            print 'input dtype:             {}'.format(self._input_dtype)
+            print 'input transform shape:   {}'.format(self._input_transform_shape)
+            print 'input shape:             {}'.format(self._input_shape)
+            print 'input slices:            {}'.format(self._input_slices)
+            print ':OUTPUT:'
+            print 'output axes:             {}'.format(self._output_axes)
+            print 'output_dtype:            {}'.format(self._output_dtype)
+            print 'output transform shape:  {}'.format(self._output_transform_shape)
+            print 'output shape:            {}'.format(self._output_shape)
+            print 'output_slices:           {}'.format(self._output_slices)
+            print ':TRANSFORM INFO:'
+            print 'transform_info:          {}'.format(axis_format(transform_info))
+            print ':TRANSPOSE INFO:'
+            print 'transpose_info:          {}'.format(axis_format(transpose_info))
+            print ':ZERO FILL:'
+            print 'zero_fill_output_slices: {}'.format(slc_format(self._zero_fill_output_slices))
+    
+    def get_mapped_input_buffer(self):
+        return self.get_mapped_full_input_buffer()[self.input_slices]
+    def get_mapped_output_buffer(self):
+        return self.get_mapped_full_output_buffer()[self.output_slices]
+    def get_mapped_full_input_buffer(self):
+        dfield = self._dfield
+        if (self.is_forward 
+             and dfield.backend.kind == Backend.OPENCL
+             and self.transform_group._op.enable_opencl_host_buffer_mapping):
+            return self.transform_group._op.get_mapped_object(dfield)[dfield.compute_slices]
+        else:
+            return self.full_input_buffer
+    def get_mapped_full_output_buffer(self):
+        dfield = self._dfield
+        if (self.is_backward
+             and dfield.backend.kind == Backend.OPENCL
+             and self.transform_group._op.enable_opencl_host_buffer_mapping):
+            return self.transform_group._op.get_mapped_object(dfield)[dfield.compute_slices]
+        else:
+            return self.full_output_buffer
+
+    def determine_buffer_shape(cls, transform_shape, target_is_buffer, offsets, axes): 
+        offsets = tuple(offsets[ai] for ai in axes)
+        slices = []
+        shape = []
+        zero_fill_slices = []
+        dim = len(axes)
+        for i,((lo,ro),si) in enumerate(zip(offsets, transform_shape)):
+            if (lo^ro) and target_is_buffer:
+                Si = si
+                slc = slice(0, si)
+            else:
+                Si = si+lo+ro
+                slc = slice(lo, Si-ro)
+                if (lo>0):
+                    zfill = [slice(None,None,None)]*dim
+                    zfill[i] = slice(0,lo)
+                    zfill = tuple(zfill)
+                    zero_fill_slices.append(zfill)
+                if (ro>0):
+                    zfill = [slice(None,None,None)]*dim
+                    zfill[i] = slice(Si-ro,Si)
+                    zfill = tuple(zfill)
+                    zero_fill_slices.append(zfill)
+            shape.append(Si)
+            slices.append(slc)
+        return tuple(shape), tuple(slices), tuple(zero_fill_slices)
+
+    def configure_input_buffer(self, buf):
+        input_dtype, input_shape = self.input_dtype, self.input_shape
+        buf_nbytes   = compute_nbytes(buf.shape, buf.dtype)
+        input_nbytes = compute_nbytes(input_shape, input_dtype) 
+        assert buf_nbytes >= input_nbytes, (buf_nbytes, input_nbytes)
+        if (buf.shape!=input_shape) or (buf.dtype!=input_dtype):
+            buf = buf.view(dtype=np.int8)[:input_nbytes].view(dtype=input_dtype).reshape(input_shape)
+        if isinstance(buf, Array):
+            buf = buf.handle
+        input_buffer = buf[self.input_slices]
+        assert input_buffer.shape == self.input_transform_shape
+        self._full_input_buffer = buf
+        self._input_buffer      = input_buffer
+        for symbol in self._input_symbolic_arrays:
+            symbol.to_backend(self.backend.kind).bind_memory_object(buf)
+        return input_buffer
+
+
+    def configure_output_buffer(self, buf):
+        output_dtype, output_shape = self.output_dtype, self.output_shape
+        buf_nbytes    = compute_nbytes(buf.shape, buf.dtype)
+        output_nbytes = compute_nbytes(output_shape, output_dtype) 
+        assert buf_nbytes >= output_nbytes, (buf_nbytes, output_nbytes)
+        if (buf.shape!=output_shape) or (buf.dtype!=output_dtype):
+            buf = buf.view(dtype=np.int8)[:output_nbytes].view(dtype=output_dtype).reshape(output_shape)
+        if isinstance(buf, Array):
+            buf = buf.handle
+        output_buffer = buf[self.output_slices]
+        assert output_buffer.shape == self.output_transform_shape
+        self._full_output_buffer = buf
+        self._output_buffer      = output_buffer
+        for symbol in self._output_symbolic_arrays:
+            symbol.to_backend(self.backend.kind).bind_memory_object(buf)
+        return output_buffer
+
+    def _discretize_forward(self, field_axes, **kwds):
+        dfield = self.op.input_discrete_fields[self.field]
+       
+        grid_resolution  = dfield.mesh.grid_resolution
+        local_resolution = dfield.compute_resolution
+
+        input_dtype = dfield.dtype
+        dim         = dfield.dim
+        
+        forward_transforms  = self.transforms[::-1]
+        backward_transforms = STU.get_inverse_transforms(*forward_transforms)
+        
+        (resolution, transform_offsets) = \
+                STU.get_transform_resolution(local_resolution, *forward_transforms)
+
+        local_transform_info = self._determine_transform_info(forward_transforms,
+                                                              resolution, input_dtype)
+        local_transpose_info = self._determine_transpose_info(field_axes, 
+                                                              local_transform_info)
+        
+        local_transform_info = self._permute_transform_info(local_transform_info, 
+                                                            local_transpose_info)
+
+        transform_info = local_transform_info
+        transpose_info = local_transpose_info
+
+        return (dfield, transform_info, transpose_info, 
+                transform_offsets)
+
+
+    def _discretize_backward(self, field_axes, **kwds):
+        
+        forward_transforms  = self.transforms[::-1]
+        backward_transforms = STU.get_inverse_transforms(*forward_transforms)
+
+        def reverse_transform_info(transform_info):
+            transform_info = list(transform_info)
+            for (i,d) in enumerate(transform_info):
+                d = list(d)
+                d[1] = forward_transforms[i]
+                d2,d3 = d[2:4]
+                d[2:4] = d3,d2
+                transform_info[i] = tuple(d)
+            transform_info = tuple(transform_info)
+            return transform_info[::-1]
+
+        def reverse_transpose_info(transpose_info):
+            transpose_info = list(transpose_info)
+            for (i,d) in enumerate(transpose_info):
+                if (d[0] is not None):
+                    d = list(d)
+                    d1,d2,d3,d4 = d[1:5]
+                    d[1:5] = d2,d1,d4,d3
+                    d[0] = tuple(d[1].index(ai) for ai in d[2])
+                    d = tuple(d)
+                else:
+                    # no permutation
+                    assert d[1]==d[2]
+                    assert d[3]==d[4]
+                transpose_info[i] = d
+            return transpose_info[::-1]
+
+        dfield = self.op.output_discrete_fields[self.field]
+
+        grid_resolution  = dfield.mesh.grid_resolution
+        local_resolution = dfield.compute_resolution
+
+        output_dtype = dfield.dtype
+        dim          = dfield.dim
+        
+        (resolution, transform_offsets) = \
+                STU.get_transform_resolution(local_resolution, *backward_transforms)
+
+        local_backward_transform_info = self._determine_transform_info(backward_transforms, 
+                                                        resolution, output_dtype)
+        local_backward_transpose_info = self._determine_transpose_info(field_axes, 
+                                                        local_backward_transform_info)
+        local_backward_transform_info = self._permute_transform_info(
+                                            local_backward_transform_info, 
+                                            local_backward_transpose_info)
+        
+        local_forward_transform_info  = reverse_transform_info(local_backward_transform_info)
+        local_forward_transpose_info  = reverse_transpose_info(local_backward_transpose_info)
+        
+        transform_info = local_forward_transform_info
+        transpose_info = local_forward_transpose_info
+
+        return (dfield, transform_info, transpose_info, 
+                    transform_offsets)
+        
+    
+    @classmethod
+    def _determine_transform_info(cls, transforms, src_shape, src_dtype):
+        transform_info = []
+        dim = len(transforms)
+        dst_shape, dst_dtype = src_shape, src_dtype
+        dst_view = [slice(0,si) for si in src_shape]
+        for (i,tr) in enumerate(transforms):
+            axis = i
+            src_shape = dst_shape
+            src_dtype = dst_dtype
+            src_view  = dst_view
+            if STU.is_none(tr):
+                pass
+            elif STU.is_backward(tr):
+                msg='{} is not a forward transform.'
+                msg=msg.format(tr)
+                raise ValueError(msg)
+            elif STU.is_R2R(tr):
+                msg='Expected a floating point data type but got {}.'.format(src_dtype)
+                assert is_fp(src_dtype), msg
+                # data type and shape does not change
+            elif STU.is_R2C(tr):
+                msg='Expected a floating point data type but got {}.'.format(src_dtype)
+                assert is_fp(src_dtype), msg
+                dst_shape = list(src_shape) 
+                dst_shape[dim-axis-1] = dst_shape[dim-axis-1]//2 + 1
+                dst_shape = tuple(dst_shape)
+                dst_dtype = float_to_complex_dtype(src_dtype)
+            elif STU.is_C2C(tr):
+                msg='Expected a complex data type but got {}.'.format(src_dtype)
+                assert is_complex(src_dtype), msg
+                # data type and shape does not change
+            else:
+                msg='Unknown transform type {}.'.format(tr)
+                raise ValueError(msg)
+            
+            
+            (lo,ro) = STU.get_transform_offsets(tr) 
+            src_view = src_view[:] 
+            src_view[dim-axis-1] = slice(lo, src_shape[dim-axis-1]-ro)
+            
+            dst_view = src_view[:]
+            dst_view[dim-axis-1] = slice(lo, dst_shape[dim-axis-1]-ro)
+
+            src_dtype = np.dtype(src_dtype)
+            dst_dtype = np.dtype(dst_dtype)
+
+            data = (axis, tr, (src_shape, src_dtype, tuple(src_view)), 
+                              (dst_shape, dst_dtype, tuple(dst_view)))
+            transform_info.append(data)
+        transform_info = tuple(transform_info)
+        return transform_info
+    
+    @classmethod
+    def _determine_transpose_info(cls, src_axes, transform_info):
+        transpose_info = []
+        dim = len(src_axes)
+        for (axis, tr, (src_shape, src_dtype, src_view), 
+                       (dst_shape, dst_dtype, dst_view)) in transform_info:
+            dst_axis = dim - 1 - axis
+            if (not STU.is_none(tr)) and (dst_axis != src_axes[-1]):
+                idx = src_axes.index(dst_axis)
+                dst_axes = list(src_axes)
+                dst_axes[idx] = src_axes[-1]
+                dst_axes[-1]  = dst_axis
+                dst_axes    = tuple(dst_axes)
+                permutation = tuple(src_axes.index(ai) for ai in dst_axes)
+            else:
+                dst_axes = src_axes
+                permutation = None
+
+            dst_shape = tuple(src_shape[ai] for ai in dst_axes)
+            src_shape = tuple(src_shape[ai] for ai in src_axes)
+
+            data = (permutation, src_axes, dst_axes, src_shape, dst_shape)
+
+            transpose_info.append(data)
+            src_axes = dst_axes
+        transpose_info = tuple(transpose_info)
+        return transpose_info
+   
+    @classmethod
+    def _permute_transform_info(cls, transform_info, transpose_info):
+        assert len(transform_info)==len(transpose_info)
+        transform_info = list(transform_info)
+        for i,(transpose, transform) in enumerate(zip(transpose_info, transform_info)):
+            (_, _, dst_axes, _, transpose_out_shape) = transpose
+            (_1,_2,(src_shape,_3,src_view), (dst_shape,_4,dst_view)) = transform
+            permuted_src_shape = tuple(src_shape[ai] for ai in dst_axes)
+            permuted_src_view  = tuple(src_view[ai]  for ai in dst_axes)
+            permuted_dst_shape = tuple(dst_shape[ai] for ai in dst_axes)
+            permuted_dst_view  = tuple(dst_view[ai]  for ai in dst_axes)
+            assert (permuted_src_shape == transpose_out_shape)
+            transform = (_1,_2,(permuted_src_shape,_3,permuted_src_view), 
+                               (permuted_dst_shape,_4,permuted_dst_view))
+            transform_info[i] = transform
+        transform_info = tuple(transform_info)
+        return transform_info
+    
+    @_discretized
+    def get_mem_requests(self, **kwds):
+
+        # first we need to find out src and dst buffers for transforms (B0 and B1)
+        nbytes = 0
+        for (_, _, (src_shape, src_dtype, src_view), 
+                   (dst_shape, dst_dtype, dst_view)) in self._transform_info:
+            nbytes = max(nbytes, compute_nbytes(src_shape, src_dtype))
+            nbytes = max(nbytes, compute_nbytes(dst_shape, dst_dtype))
+        nbytes = max(nbytes, compute_nbytes(self.input_shape, self.input_dtype))
+        nbytes = max(nbytes, compute_nbytes(self.output_shape, self.output_dtype))
+
+        # Then we need to find out the size of an additional tmp buffer
+        # we can only do it by creating temporary plans prior to setup
+        # with temporary buffers.
+        tmp_nbytes = 0
+        tg  = self.transform_group
+        src = tg.FFTI.backend.empty(shape=(nbytes,), dtype=np.uint8, min_alignment=tg.op.min_fft_alignment) 
+        dst = tg.FFTI.backend.empty(shape=(nbytes,), dtype=np.uint8, min_alignment=tg.op.min_fft_alignment) 
+        queue = tg.FFTI.new_queue(tg=tg, name='tmp_queue')
+        for (_, tr, (src_shape, src_dtype, src_view), 
+                    (dst_shape, dst_dtype, dst_view)) in self._transform_info:
+            src_nbytes = compute_nbytes(src_shape, src_dtype)
+            dst_nbytes = compute_nbytes(dst_shape, dst_dtype)
+            b0 = src[:src_nbytes].view(dtype=src_dtype).reshape(src_shape)
+            b1 = dst[:dst_nbytes].view(dtype=dst_dtype).reshape(dst_shape)
+            fft_plan = tg.FFTI.get_transform(tr)(a=b0.handle, out=b1.handle, 
+                                                  axis=self.field.dim-1,
+                                                  verbose=False)
+            fft_plan.setup(queue=queue)
+            tmp_nbytes = max(tmp_nbytes, fft_plan.required_buffer_size)
+
+        if (tmp_nbytes > nbytes):
+            msg='Planner claims to need more than buffer bytes as temporary buffer:'
+            msg+='\n  *Buffer bytes: {}'.format(bytes2str(nbytes))
+            msg+='\n  *Tmp    bytes: {}'.format(bytes2str(tmp_nbytes))
+            warnings.warn(msg, HysopFFTWarning)
+
+        backend = self.transform_group.backend
+        mem_tag = self.transform_group.mem_tag
+        kind    = backend.kind
+        
+        B0_tag  = '{}_{}_B0'.format(mem_tag, kind)
+        B1_tag  = '{}_{}_B1'.format(mem_tag, kind)
+        TMP_tag = '{}_{}_TMP'.format(mem_tag, kind)
+        self.B0_tag, self.B1_tag, self.TMP_tag = B0_tag, B1_tag, TMP_tag
+
+        return {B0_tag:  nbytes,
+                B1_tag:  nbytes,
+                TMP_tag: tmp_nbytes}
+    
+    @_discretized
+    def setup(self, work):
+        SETUP_DEBUG = False
+        assert not self.ready
+       
+        dim  = self.field.dim
+        op   = self.op
+        tg   = self.transform_group
+        FFTI = tg.FFTI
+        
+        is_forward  = self.is_forward
+        is_backward = self.is_backward
+
+        ntransforms    = self._ntransforms
+        transform_info = self._transform_info
+        transpose_info = self._transpose_info
+        B0_tag, B1_tag = self.B0_tag, self.B1_tag
+        TMP_tag        = self.TMP_tag
+
+        # get temporary buffers
+        B0,  = work.get_buffer(op, B0_tag,  handle=True)
+        B1,  = work.get_buffer(op, B1_tag,  handle=True)
+        assert is_byte_aligned(B0)
+        assert is_byte_aligned(B1)
+
+        try:
+            TMP, = work.get_buffer(op, TMP_tag, handle=True) 
+        except ValueError:
+            TMP = None
+        
+        # bind field buffer to input or output
+        dfield = self.dfield
+        if is_forward:
+            self.configure_input_buffer(dfield.sbuffer[dfield.compute_slices])
+        else:
+            self.configure_output_buffer(dfield.sbuffer[dfield.compute_slices])
+
+        # bind group buffer to input or output if required.
+        custom_input_buffer  = self._custom_input_buffer
+        custom_output_buffer = self._custom_output_buffer
+        if (is_forward and custom_output_buffer):
+            if (custom_output_buffer=='auto'):
+                # will be determined and set later
+                pass
+            elif (custom_output_buffer=='B0'):
+                self.configure_output_buffer(B0)
+            elif (custom_output_buffer=='B1'):
+                self.configure_output_buffer(B1)
+            else:
+                msg='Unknown custom output buffer {}.'.format(custom_output_buffer)
+                raise NotImplementedError(msg)
+        if (is_backward and custom_input_buffer):
+            if (custom_input_buffer=='auto'):
+                assert self._matching_forward_transform.ready
+                custom_input_buffer = self._matching_forward_transform._custom_output_buffer
+                assert custom_input_buffer in ('B0', 'B1')
+            if (custom_input_buffer=='B0'):
+                self.configure_input_buffer(B0)
+            elif (custom_input_buffer=='B1'):
+                self.configure_input_buffer(B1)
+            else:
+                msg='Unknown custom input buffer {}.'.format(custom_input_buffer)
+                raise NotImplementedError(msg)
+        
+        # define input and output buffer, as well as tmp buffers
+        src_buffer, dst_buffer = B0, B1
+        def nameof(buf):
+            assert (buf is B0) or (buf is B1)
+            if (buf is B0):
+                return 'B0'
+            else:
+                return 'B1'
+
+        def check_size(buf, nbytes, name):
+            if (buf.nbytes < nbytes):
+                msg='Insufficient buffer size for buffer {} (shape={}, dtype={}).'.format(name, buf.shape, buf.dtype)
+                msg+='\nExpected at least {} bytes but got {}.'.format(nbytes, buf.nbytes)
+                try:
+                    bname = nameof(buf)
+                    msg+='\nThis buffer has been identified as {}.'.format(bname)
+                except:
+                    pass
+                raise RuntimeError(msg)
+        
+        # build spectral transform execution queue
+        qname = 'fft_planner_{}_{}'.format(self.field.name, 
+                'forward' if is_forward else 'backward')
+        queue = FFTI.new_queue(tg=self, name=qname)
+
+        if SETUP_DEBUG:
+            def print_op(description, category):
+                prefix = '     |> '
+                print '{}{: <40}[{}]'.format(prefix, description, category)
+        
+            msg='''
+SPECTRAL TRANSFORM SETUP
+  op:          {}
+  dim:         {}
+  ntransforms: {}
+  group_tag:   {}
+  is_forward:  {}
+  is_backward: {}'''.format(
+           op.pretty_tag,
+           dim, ntransforms, self.tag, 
+           is_forward, is_backward)
+            print msg
+
+        fft_plans = ()
+        for i in xrange(ntransforms):
+            transpose = transpose_info[i]
+            transform = transform_info[i]
+            (permutation, _, _, input_shape, output_shape) = transpose
+            (_, tr, (src_shape, src_dtype, src_view), 
+                    (dst_shape, dst_dtype, dst_view)) = transform
+            assert not STU.is_none(tr), 'Got a NONE transform type.'
+            
+            is_first = (i==0)
+            is_last  = (i==ntransforms-1)
+
+            should_forward_permute  = (is_forward  and (permutation is not None))
+            should_backward_permute = (is_backward and (permutation is not None))
+            
+            if SETUP_DEBUG:
+                msg='  TRANSFORM INDEX {}:'.format(i)
+
+                if (permutation is not None):
+                    msg+='''
+    Transpose Info: 
+      permutation:  {}
+      input_shape:  {}
+      output_shape: {}
+      forward_permute:  {}
+      backward_permute: {}'''.format(
+            permutation, input_shape, output_shape,
+            should_forward_permute, should_backward_permute)
+
+                msg+='''
+    Custom buffers:
+      custom_input:  {}
+      custom output: {}
+    Transform Info:
+      SRC: shape {} and type {}, view {}
+      DST: shape {} and type {}, view {}
+    Planned Operations:'''.format(
+            custom_input_buffer, custom_output_buffer,
+            src_shape, src_dtype, src_view,
+            dst_shape, dst_dtype, dst_view)
+                print msg
+            
+            src_nbytes = compute_nbytes(src_shape, src_dtype)
+            dst_nbytes = compute_nbytes(dst_shape, dst_dtype)
+
+            # build forward permutation if required 
+            # (forward transforms transpose before actual transforms)
+            if should_forward_permute:
+                input_nbytes  = compute_nbytes(input_shape,  src_dtype)
+                output_nbytes = compute_nbytes(output_shape, src_dtype)
+                assert output_shape == src_shape, 'Transpose to Transform shape mismatch.'
+                assert input_nbytes == output_nbytes, 'Transpose input and output size mismatch.'
+                assert src_buffer.nbytes >= input_nbytes,  'Insufficient buffer size for src buf.'
+                assert dst_buffer.nbytes >= output_nbytes, 'Insufficient buffer size for dst buf.'
+                if is_first:
+                    assert (self.input_buffer.shape == input_shape), 'input_buffer shape mismatch.'
+                    assert (self.input_buffer.dtype == src_dtype), 'input_buffer dtype mismatch.'
+                    b0 = self.get_mapped_input_buffer
+                else:
+                    b0 = src_buffer[:input_nbytes].view(dtype=src_dtype).reshape(input_shape)
+                b1 = dst_buffer[:output_nbytes].view(dtype=src_dtype).reshape(output_shape)
+                queue += FFTI.plan_transpose(tg=tg, src=b0, dst=b1, axes=permutation)
+                if SETUP_DEBUG:
+                    sfrom='input_buffer' if is_first else nameof(src_buffer)
+                    sto=nameof(dst_buffer)
+                    print_op('PlanTranspose(src={}, dst={})'.format(sfrom, sto, permutation),
+                             'forward permute')
+                src_buffer, dst_buffer = dst_buffer, src_buffer
+            elif is_first:
+                assert (self.input_buffer.shape == src_shape), 'input buffer shape mismatch.'
+                assert (self.input_buffer.dtype == src_dtype), 'input buffer dtype mismatch.'
+                assert src_buffer.nbytes >= src_nbytes, 'Insufficient buffer size for src buf.'
+                if ((custom_input_buffer is not None) and 
+                        (nameof(src_buffer) == custom_input_buffer)):
+                    src_buffer, dst_buffer = dst_buffer, src_buffer
+                b0 = src_buffer[:src_nbytes].view(dtype=src_dtype).reshape(src_shape)
+                queue += FFTI.plan_copy(tg=tg, src=self.get_mapped_input_buffer, dst=b0)
+                if SETUP_DEBUG:
+                    sfrom='input_buffer'
+                    sto=nameof(src_buffer)
+                    print_op('PlanCopy(src={}, dst={})'.format(sfrom, sto),
+                             'pre-transform copy')
+
+            # build batched 1D transform in contiguous axis
+            check_size(src_buffer, src_nbytes, 'src')
+            check_size(dst_buffer, dst_nbytes, 'dst')
+            b0 = src_buffer[:src_nbytes].view(dtype=src_dtype).reshape(src_shape)
+            b1 = dst_buffer[:dst_nbytes].view(dtype=dst_dtype).reshape(dst_shape)
+            fft_plan = FFTI.get_transform(tr)(a=b0, out=b1, axis=dim-1)
+            fft_plan.setup(queue=queue)
+            fft_plans += (fft_plan,)
+            queue += fft_plan
+            if SETUP_DEBUG:
+                sfrom=nameof(src_buffer)
+                sto=nameof(dst_buffer)
+                print_op('PlanTransform(src={}, dst={})'.format(sfrom, sto), tr)
+            src_buffer, dst_buffer = dst_buffer, src_buffer
+            
+            # build backward permutation if required 
+            # (backward transforms transpose after actual transforms)
+            if should_backward_permute:
+                input_nbytes  = compute_nbytes(input_shape,  dst_dtype)
+                output_nbytes = compute_nbytes(output_shape, dst_dtype)
+                assert input_shape == dst_shape, 'Transform to Transpose shape mismatch.'
+                assert input_nbytes == output_nbytes, 'Transpose input and output size mismatch.'
+                assert src_buffer.nbytes >= input_nbytes,  'Insufficient buffer size for src buf.'
+                assert dst_buffer.nbytes >= output_nbytes, 'Insufficient buffer size for dst buf.'
+                b0 = src_buffer[:input_nbytes].view(dtype=dst_dtype).reshape(input_shape)
+                if is_last and (self._action is SpectralTransformAction.OVERWRITE):
+                    assert (self.output_buffer.shape == output_shape), \
+                            'output buffer shape mismatch.'
+                    assert (self.output_buffer.dtype == dst_dtype), \
+                            'output buffer dtype mismatch.'
+                    b1 = self.get_mapped_output_buffer
+                else:
+                    b1 = dst_buffer[:output_nbytes].view(dtype=dst_dtype).reshape(output_shape)
+                queue += FFTI.plan_transpose(tg=tg, src=b0, dst=b1, axes=permutation)
+                if SETUP_DEBUG:
+                    sfrom=nameof(src_buffer)
+                    sto='output_buffer' if is_last else nameof(dst_buffer)
+                    print_op('PlanTranspose(src={}, dst={})'.format(sfrom, sto),
+                             'backward permute')
+                src_buffer, dst_buffer = dst_buffer, src_buffer
+                if is_last and (self._action is not SpectralTransformAction.OVERWRITE):
+                    if (self._action is SpectralTransformAction.ACCUMULATE):
+                        assert (self.output_buffer.shape == output_shape), \
+                                'output buffer shape mismatch.'
+                        assert (self.output_buffer.dtype == dst_dtype), \
+                                'output buffer dtype mismatch.'
+                        queue += FFTI.plan_accumulate(tg=tg, src=b1, dst=self.get_mapped_output_buffer)
+                        if SETUP_DEBUG:
+                            sfrom=nameof(dst_buffer)
+                            sto='output_buffer'
+                            print_op('PlanAccumulate(src={}, dst={})'.format(sfrom, sto),
+                                     'post-transform accumulate')
+                    else:
+                        msg='Unsupported action {}.'.format(self._action)
+                        raise NotImplementedError(msg)
+
+            elif is_last:
+                if (custom_output_buffer is not None): 
+                    if custom_output_buffer not in ('B0', 'B1', 'auto'):
+                        msg='Unknown custom output buffer {}.'.format(custom_output_buffer)
+                        raise NotImplementedError(msg)
+                    elif (custom_output_buffer=='auto'):
+                        custom_output_buffer = nameof(dst_buffer)
+                        self._custom_output_buffer = custom_output_buffer
+                        if (custom_output_buffer=='B0'):
+                            self.configure_output_buffer(B0)
+                        elif (custom_output_buffer=='B1'):
+                            self.configure_output_buffer(B1)
+                        else:
+                            raise RuntimeError
+                    elif (nameof(src_buffer) == custom_output_buffer):
+                        # This is a special case where we need to copy back and forth 
+                        # (because of offsets)
+                        b0 = src_buffer[:dst_nbytes].view(dtype=dst_dtype).reshape(dst_shape)
+                        b1 = dst_buffer[:dst_nbytes].view(dtype=dst_dtype).reshape(dst_shape)
+                        queue += FFTI.plan_copy(tg=tg, src=b0, dst=b1)
+                        if SETUP_DEBUG:
+                            sfrom=nameof(src_buffer)
+                            sto=nameof(dst_buffer)
+                            print_op('PlanCopy(src={}, dst={})'.format(sfrom, sto),
+                                     'post-transform copy')
+                        src_buffer, dst_buffer = dst_buffer, src_buffer
+                assert (self.output_buffer.shape == dst_shape), 'output buffer shape mismatch.'
+                assert (self.output_buffer.dtype == dst_dtype), 'output buffer dtype mismatch.'
+                assert src_buffer.nbytes >= dst_nbytes, 'Insufficient buffer size for src buf.'
+                b0 = src_buffer[:dst_nbytes].view(dtype=dst_dtype).reshape(dst_shape)
+                if self._action is SpectralTransformAction.OVERWRITE:
+                    pname='PlanCopy'
+                    pdes='post-transform-copy'
+                    queue += FFTI.plan_copy(tg=tg, src=b0, dst=self.get_mapped_output_buffer)
+                elif self._action is SpectralTransformAction.ACCUMULATE:
+                    pname='PlanAccumulate'
+                    pdes='post-transform-accumulate'
+                    queue += FFTI.plan_accumulate(tg=tg, src=b0, dst=self.get_mapped_output_buffer)
+                else:
+                    msg='Unsupported action {}.'.format(self._action)
+                    raise NotImplementedError(msg)
+                if SETUP_DEBUG:
+                    sfrom=nameof(src_buffer)
+                    sto='output_buffer' if (custom_output_buffer is None) \
+                                        else custom_output_buffer
+                    print_op('{}(src={}, dst={})'.format(pname, sfrom, sto),
+                             pdes)
+    
+        if self._zero_fill_output_slices:
+            buf  = self.get_mapped_full_output_buffer
+            slcs = self._zero_fill_output_slices
+            queue += FFTI.plan_fill_zeros(tg=tg, a=buf, slices=slcs)
+            if SETUP_DEBUG:
+                print_op('PlanFillZeros(dst=output_buffer)',
+                         'post-transform-callback')
+
+        # allocate fft plans
+        FFTI.allocate_plans(op, fft_plans, tmp_buffer=TMP)
+
+        self._queue = queue
+        self._ready = True
+
+    def __call__(self):
+        assert (self._ready)
+        assert (self._queue is not None)
+        return self._queue.execute()
+
diff --git a/hysop/operator/curl.py b/hysop/operator/curl.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed68c1bcb6f8facfdcfd7809b791a7ffa8d0184c
--- /dev/null
+++ b/hysop/operator/curl.py
@@ -0,0 +1,84 @@
+
+from hysop.constants import Implementation
+from hysop.fields.continuous_field import Field
+from hysop.tools.types import check_instance, first_not_None, to_tuple
+from hysop.tools.decorators import debug
+from hysop.core.graph.computational_node_frontend import ComputationalGraphNodeFrontend
+from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
+
+class Curl(ComputationalGraphNodeFrontend):
+    """Generate an operator to compute the curl of a Field."""
+
+    @classmethod
+    def fd(*args, **kwds):
+        return FiniteDifferencesCurl(*args, **kwds)
+    
+    @classmethod
+    def spectral(*args, **kwds):
+        return SpectralCurl(*args, **kwds)
+
+
+    @classmethod
+    def implementations(cls):
+        raise NotImplementedError
+
+    @classmethod
+    def default_implementation(cls):
+        raise NotImplementedError
+
+    @debug
+    def __init__(self, Fin, Fout, variables, 
+                    implementation=None, base_kwds=None, **kwds):
+        """
+        Create an operator that computes the curl of an input field Fin.
+
+        Given Fin, a 2D ScalarField or VectorField or a 3D VectorField, compute Fout = curl(Fin).
+        
+        Only the following configurations are supported:
+                 dim   nb_components  |   dim   nb_components
+        Input:    2        (1,2)      |    3          3
+        Output:   2        (2,1)      |    3          3
+
+        Parameters
+        ----------
+        Fin: hysop.field.continuous_field.Field
+            Continuous field as input ScalarField or VectorField.
+            All contained field have to live on the same domain.
+        Fout: hysop.field.continuous_field.Field
+            Continuous field as output VectorField.
+            All contained field have to live on the same domain.
+        variables: dict
+            dictionary of fields as keys and topologies as values.
+        implementation: Implementation, optional, defaults to None
+            target implementation, should be contained in available_implementations().
+            If None, implementation will be set to default_implementation().
+        kwds: dict, optional
+            Extra parameters passed towards base class (MultiSpaceDerivatives).
+        """
+        base_kwds = first_not_None(base_kwds, {})
+        check_instance(Fin, Field)
+        check_instance(Fout, Field)
+        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
+
+        super(Curl, self).__init__(Fin=Fin, Fout=Fout, variables=variables,
+                candidate_input_tensors=(Fin,),
+                candidate_output_tensors=(Fout,),
+                implementation=implementation, 
+                base_kwds=base_kwds, **kwds)
+
+class SpectralCurl(Curl):
+    @classmethod
+    def implementations(cls):
+        from hysop.backend.host.python.operator.curl import PythonSpectralCurl
+        from hysop.backend.device.opencl.operator.curl import OpenClSpectralCurl
+        __implementations = {
+                Implementation.PYTHON: PythonSpectralCurl,
+                Implementation.OPENCL: OpenClSpectralCurl,
+        }
+        return __implementations
+    
+    @classmethod
+    def default_implementation(cls):
+        return Implementation.PYTHON
+
+
diff --git a/hysop/operator/derivative.py b/hysop/operator/derivative.py
index 0c157b034e1a09bff3a55a8825b99c3b5d5877e9..4c958199797915c8f3e54e69271adf5f1cba7c25 100644
--- a/hysop/operator/derivative.py
+++ b/hysop/operator/derivative.py
@@ -1,5 +1,3 @@
-"""Initialize fields on a grid, with a user-defined function
-"""
 from hysop.constants import Implementation, DirectionLabels, TranspositionState
 from hysop.fields.continuous_field import Field, ScalarField, TensorField
 from hysop.tools.types import check_instance, first_not_None, to_tuple
@@ -11,25 +9,30 @@ from hysop.core.graph.node_generator import ComputationalGraphNodeGenerator
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
 from hysop.operator.directional.directional import DirectionalOperatorGeneratorI
 
+
 class SpaceDerivative(ComputationalGraphNodeFrontend):
     """
     Operator frontend to compute the derivative of a component
-    of a field in a given direction.
+    of a field in a given direction using a given method.
+
+    Currently two methods are supported:
+        *Finite differences: FiniteDifferencesSpaceDerivative
+        *Spectral method:    SpectralSpaceDerivative
+
+    Those two classes can be passed to the more general MultiSpaceDerivatives
+    and Gradient operator generators via the 'cls' keyword argument during
+    __init__.
     """
 
     @classmethod
-    def implementations(cls):
-        from hysop.backend.host.python.operator.derivative   import PythonSpaceDerivative
-        from hysop.backend.device.opencl.operator.derivative import OpenClSpaceDerivative
-        __implementations = {
-                Implementation.PYTHON: PythonSpaceDerivative,
-                Implementation.OPENCL: OpenClSpaceDerivative
-        }
-        return __implementations
+    def spectral(cls, *args, **kwds):
+        """SpaceDerivative.spectral(...) <=> SpectralSpaceDerivative(...)"""
+        return SpectralSpaceDerivative(*args, **kwds)
 
     @classmethod
-    def default_implementation(cls):
-        return Implementation.PYTHON
+    def fd(cls, *args, **kwds):
+        """SpaceDerivative.fd(...) <=> FiniteDifferencesSpaceDerivative(...)"""
+        return FiniteDifferencesSpaceDerivative(*args, **kwds)
 
     @debug
     def __init__(self, F, dF, A=None,
@@ -43,11 +46,15 @@ class SpaceDerivative(ComputationalGraphNodeFrontend):
         Compute the derivative of a field in a given direction
         on a given backend, possibly scaled by another field/parameter/value.
 
-        dF = A * dF/dxj
+        dF = A * dF^m/d(x0^k0 x1^k1 ... xn^kn)
+            where k
 
         where F  is an input field
               dF is an output field
               A is a Field, a Parameter or a scalar.
+              (k0,...,kn) are positive integers
+              m=sum(ki)
+              n=F.dim-1
 
         Parameters
         ----------
@@ -58,11 +65,14 @@ class SpaceDerivative(ComputationalGraphNodeFrontend):
             Some backend may allow inplace differentiation.
         A: numerical value, ScalarParameter or Field, optional
             Scaling for convenience.
-        derivative: int, optional
-            Which derivative to generate, defaults to 1.
+        derivative: int or tuple, optional
+            Which derivative to generate, defaults to (0,)*(dim-1)+(1,).
+            ie. first order derivative in X axis.
+            If integer is given, the derivative is taken in given direction.
         direction: int, optional
             Directions in which to take the derivative.
-            Defaults to 0.
+            Defaults to None.
+            Should be None if derivative is a tuple.
         name: str, optional
             Name of this operator.
         pretty_name: str, optional
@@ -76,10 +86,20 @@ class SpaceDerivative(ComputationalGraphNodeFrontend):
             Base class keyword arguments.
         kwds: dict, optional
             Extra parameters passed towards operator implementation.
+        
+        Notes
+        -----
+        There is two way to build a derivative:
+            (1) derivative(int) + direction(int) gives:
+                 => derivative=(0,0,0,0,kd,0,0,0)
+                    where the index of kd is direction
+                    and kd=derivative
+            (2) derivative(tuple) + direction(None) gives:
+                 => derivative=(k0,...,kn)
         """
         check_instance(F,  ScalarField)
         check_instance(dF, ScalarField)
-        check_instance(derivative, int, allow_none=True)
+        check_instance(derivative, (tuple,int), allow_none=True)
         check_instance(direction, int, allow_none=True)
         check_instance(variables, dict, keys=Field,
                 values=CartesianTopologyDescriptors, allow_none=True)
@@ -96,37 +116,70 @@ class SpaceDerivative(ComputationalGraphNodeFrontend):
                 variables=variables, implementation=implementation,
                 base_kwds=base_kwds, name=name, pretty_name=pretty_name, **kwds)
 
+    @classmethod
+    def implementations(cls):
+        raise NotImplementedError
 
-class MultiSpaceDerivatives(DirectionalOperatorGeneratorI, ComputationalGraphNodeGenerator):
-    """Generate multiple SpaceDerivative operators at once."""
+    @classmethod
+    def default_implementation(cls):
+        raise NotImplementedError
 
-    def generate_only_once_per_direction(self):
-        return True
-    
-    @debug
-    def generate_direction(self, i, dt_coeff):
-        should_generate = super(MultiSpaceDerivatives, self).generate_direction(
-                i=i, dt_coeff=dt_coeff)
-        if not should_generate:
-            return ()
-        directions = self.directions
-        ids = tuple(j for j in xrange(len(directions)) if directions[j] == i)
-        ops = tuple(self.nodes[j] for j in ids)
-        return ops
+
+
+class SpectralSpaceDerivative(SpaceDerivative):
+    """
+    Operator frontend to compute the derivative of a component
+    of a field in a given direction using spectral methods.
+    """
+    @classmethod
+    def implementations(cls):
+        from hysop.backend.host.python.operator.derivative import PythonSpectralSpaceDerivative
+        from hysop.backend.device.opencl.operator.derivative import OpenClSpectralSpaceDerivative
+        __implementations = {
+                Implementation.PYTHON: PythonSpectralSpaceDerivative,
+                Implementation.OPENCL: OpenClSpectralSpaceDerivative
+        }
+        return __implementations
+
+    @classmethod
+    def default_implementation(cls):
+        return Implementation.PYTHON
+
+
+
+class FiniteDifferencesSpaceDerivative(SpaceDerivative):
+    """
+    Operator frontend to compute the derivative of a component
+    of a field in a given direction using finite differences.
+
+    /!\ FiniteDifferencesSpaceDerivative only supports directional derivatives /!\
+    """
+    @classmethod
+    def implementations(cls):
+        from hysop.backend.host.python.operator.derivative import \
+                PythonFiniteDifferencesSpaceDerivative
+        from hysop.backend.device.opencl.operator.derivative import \
+                OpenClFiniteDifferencesSpaceDerivative
+        __implementations = {
+                Implementation.PYTHON: PythonFiniteDifferencesSpaceDerivative,
+                Implementation.OPENCL: OpenClFiniteDifferencesSpaceDerivative
+        }
+        return __implementations
     
-    @debug
-    def generate(self, **kwds):
-        if ('splitting_dim' in kwds):
-            splitting_dim = kwds['splitting_dim']
-            assert splitting_dim>max(self.directions)
-        else:
-            kwds['splitting_dim'] = max(self.directions)
-        return super(MultiSpaceDerivatives, self).generate(**kwds)
+    @classmethod
+    def default_implementation(cls):
+        return Implementation.PYTHON
+
+
+
+class MultiSpaceDerivatives(DirectionalOperatorGeneratorI, ComputationalGraphNodeGenerator):
+    """Generate multiple SpaceDerivative operators at once."""
 
     @debug
     def __init__(self, Fs, dFs, As=None, 
+             cls=FiniteDifferencesSpaceDerivative, 
              directions=None, derivatives=None, 
-             extra_params=None, cls=SpaceDerivative, base_kwds=None, 
+             extra_params=None, base_kwds=None, 
              variables=None, **op_kwds):
          """
          Create a operator generator that can handle multiple SpaceDerivative operators.
@@ -140,6 +193,15 @@ class MultiSpaceDerivatives(DirectionalOperatorGeneratorI, ComputationalGraphNod
 
          Refer to hysop.operator.derivative.SpaceDerivative for all arguments.
          """
+         from hysop.operator.min_max import MinMaxDerivativeStatistics
+         if not issubclass(cls, (SpaceDerivative, MinMaxDerivativeStatistics)) or \
+                (cls in (SpaceDerivative, MinMaxDerivativeStatistics)):
+             msg="cls should be a subclass of SpaceDerivative or MinMaxSpaceDerivativeStatistics, got {}."
+             msg+='\ncls MRO is:\n  '
+             msg+='\n  '.join(str(t) for t in cls.__mro__)
+             msg=msg.format(cls)
+             raise TypeError(msg)
+
          base_kwds    = first_not_None(base_kwds, {})
          extra_params = first_not_None(extra_params, {})
          super(MultiSpaceDerivatives, self).__init__(**base_kwds)
@@ -187,7 +249,7 @@ class MultiSpaceDerivatives(DirectionalOperatorGeneratorI, ComputationalGraphNod
          self._params = params
          self._cls = cls
          self._op_kwds = op_kwds
-         self.directions =params['direction']
+         self.directions = params['direction']
 
     @debug
     def _generate(self):
@@ -200,106 +262,28 @@ class MultiSpaceDerivatives(DirectionalOperatorGeneratorI, ComputationalGraphNod
             op = cls(**op_kwds)
             operators += (op,)
         return operators
-
-
-
-class Gradient(MultiSpaceDerivatives):
-    """Generate multiple SpaceDerivative operators to compute the gradient of a Field."""
-
-    @classmethod
-    def implementations(cls):
-        return SpaceDerivative.implementations()
-
+    
+    def generate_only_once_per_direction(self):
+        return True
+    
     @debug
-    def __init__(self, F, gradF, directions=None, implementation=None, 
-                        base_kwds=None, **kwds):
-        """
-        Create an operator generator that yields a sequence of operators
-        that compute the gradient of an input field F.
-
-        Given F, a scalar, vector or tensor field of dimension n, 
-        compute the field of dimension n+1 that is the gradient of F:
-            ScalarField: F -> gradF[j]   = dF/dxj
-            VectorField: F -> gradF[i,j] = dFi/dxj
-            TensorField: F -> gradF[i0,...,in,j] = dF[i0,...,in]/dxj
-        
-        Derivatives can be computed with respect to specific directions and not necessarily in all directions.
-        To restrict the number of components, take a tensor view on F (and gradF).
-
-        Example: if F is a VectorField of m components (F0, ..., Fm) in a domain of dimension n,
-                 this operator will compute gradF[i,j] = dF[i]/dx[j].
-                
-                    ================================
-                               dF0/dx0 ... dF0/dxn
-                                  .     .     .
-                    grad(F) =     .     .     .
-                                  .     .     .
-                               dFm/dx0 ... dFm/dxn
-                    ================================
-
-                    where F is an input field
-                          gradF is an output field != F
-                          0 <= i < nb_components
-                          0 <= j < nb_directions
-                          nb_components = F.nb_components
-                          nb_directions = min( F.dim, len(directions))
-
-        Parameters
-        ----------
-        F: hysop.field.continuous_field.Field
-            Continuous field as input (Scalar, Vector or TensorField).
-            All contained field have to live on the same domain.
-        gradF: hysop.field.continuous_field.Field
-            Continuous field to be written, should have
-            exactly shape F.shape + (ndirections,)
-        directions: tuple of ints, optional
-            The directions in which to take the derivatives.
-            Defaults to range(F.dim).
-            nb_directions = min(F.dim, len(directions))
-        implementation: Implementation, optional, defaults to None
-            target implementation, should be contained in available_implementations().
-            If None, implementation will be set to default_implementation().
-        kwds: dict, optional
-            Extra parameters passed towards base class (MultiSpaceDerivatives).
-        """
-        base_kwds = first_not_None(base_kwds, {})
-        check_instance(F, Field)
-        check_instance(gradF, Field)
-        check_instance(directions, tuple, values=int, allow_none=True)
-
-        directions = to_tuple(first_not_None(directions, range(F.dim)))
-        ndirections = len(directions)
-
-        if F.is_tensor:
-            nfields = F.size
-            oshape = F.shape + (ndirections,)
-        else:
-            nfields = 1
-            oshape = (ndirections,)
-        
-        if (gradF.is_tensor):
-            if (gradF.shape != oshape):
-                msg='Gradient field shape mismatch, expected {} but got {}.'
-                msg=msg.format(oshape, gradF.shape)
-                raise ValueError(msg)
+    def generate_direction(self, i, dt_coeff):
+        should_generate = super(MultiSpaceDerivatives, self).generate_direction(
+                i=i, dt_coeff=dt_coeff)
+        if not should_generate:
+            return ()
+        directions = self.directions
+        ids = tuple(j for j in xrange(len(directions)) if directions[j] == i)
+        ops = tuple(self.nodes[j] for j in ids)
+        return ops
+    
+    @debug
+    def generate(self, **kwds):
+        if ('splitting_dim' in kwds):
+            splitting_dim = kwds['splitting_dim']
+            assert splitting_dim>max(self.directions)
         else:
-            if (oshape != (1,)):
-                msg='Gradient field shape mismatch, expected {} but got a scalar field (ie. shape={}).'
-                msg=msg.format(oshape, (1,))
-                raise ValueError(msg)
-
-        Fs  = tuple(f for f in F.fields for d in directions)
-        dFs = gradF.fields
-        directions = tuple(d for _ in xrange(nfields) for d in directions)
-        
-        base_kwds.update(dict(
-                candidate_input_tensors=(F,),
-                candidate_output_tensors=(gradF,)))
-
-        implementation = first_not_None(implementation, SpaceDerivative.default_implementation())
-        super(Gradient, self).__init__(Fs=Fs, dFs=dFs,
-                candidate_input_tensors=(F,),
-                candidate_output_tensors=(gradF,),
-                directions=directions, implementation=implementation, 
-                base_kwds=base_kwds, **kwds)
-
+            kwds['splitting_dim'] = max(self.directions)
+        ops = super(MultiSpaceDerivatives, self).generate(**kwds)
+        assert (ops is not None), self.__class__.__mro__
+        return ops
diff --git a/hysop/operator/diffusion.py b/hysop/operator/diffusion.py
index f045823af110f7b25d15fec54adf4b0aebaccdd7..5261eaca05d5ef5aeb38bfddf32c885a4944dfa9 100644
--- a/hysop/operator/diffusion.py
+++ b/hysop/operator/diffusion.py
@@ -9,20 +9,23 @@ from hysop.tools.decorators  import debug
 from hysop.fields.continuous_field import Field
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
 from hysop.parameters.scalar_parameter import ScalarParameter
-from hysop.core.graph.computational_node_frontend import ComputationalGraphNodeFrontend
 
-from hysop.backend.host.python.operator.diffusion  import PythonDiffusion
-from hysop.backend.host.fortran.operator.diffusion import DiffusionFFTW
+from hysop.operator.base.spectral_operator import SpectralComputationalGraphNodeFrontend
+from hysop.backend.host.python.operator.diffusion   import PythonDiffusion
+from hysop.backend.device.opencl.operator.diffusion import OpenClDiffusion
+from hysop.backend.host.fortran.operator.diffusion  import DiffusionFFTW
 
-class Diffusion(ComputationalGraphNodeFrontend):
+class Diffusion(SpectralComputationalGraphNodeFrontend):
     """
     Interface the diffusion solver.
-    Available implementations are: FORTRAN: FFTW based solver 
-                                   PYTHON:  numpy.fft based solver
+    Available implementations are: FORTRAN: FFTW based solver (legacy fortran)
+                                   PYTHON:  generic python fft based solver (pyfftw, scipy or numpy)
+                                   OPENCL:  generic opencl fft based solver (gpyfft)
     """
 
     __implementations = {
             Implementation.PYTHON:  PythonDiffusion,
+            Implementation.OPENCL:  OpenClDiffusion,
             Implementation.FORTRAN: DiffusionFFTW,
     }
 
@@ -32,14 +35,15 @@ class Diffusion(ComputationalGraphNodeFrontend):
 
     @classmethod
     def default_implementation(cls):
-        return Implementation.FORTRAN
+        return Implementation.PYTHON
 
     @debug
-    def __init__(self, Fin, variables, viscosity, dt,
-            Fout=None, implementation=None, base_kwds=None, **kwds):
+    def __init__(self, Fin, variables, nu, dt,
+            Fout=None, implementation=None, 
+            base_kwds=None, **kwds):
         """
         Initialize a Poisson operator frontend.
-        Solves dF/dt = viscosity * Laplacian(F)
+        Solves dF/dt = nu * Laplacian(F)
 
         Parameters
         ----------
@@ -47,11 +51,11 @@ class Diffusion(ComputationalGraphNodeFrontend):
             input field that should be diffused
         Fout: field, optional, defaults to none
             output field that should be diffused.
-            if none this will be set to Fin.
+            if None this will be set to Fin.
         variables: dict
             dictionary of fields as keys and topologies as values.
-        viscosity: float or ScalarParameter
-            Some implementations may only offer scalar viscosity.
+        nu: float or ScalarParameter
+            Some implementations may only offer scalar nu.
         dt: ScalarParameter
             Timestep parameter that will be used for time integration.
         implementation: Implementation, optional, defaults to None
@@ -77,8 +81,9 @@ class Diffusion(ComputationalGraphNodeFrontend):
         check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
         check_instance(base_kwds, dict, keys=str)
         check_instance(dt, ScalarParameter)
-
+        check_instance(nu, (float,ScalarParameter))
+        
         super(Diffusion, self).__init__(Fin=Fin, Fout=Fout,
-                                        variables=variables, viscosity=viscosity, dt=dt,
-                                        implementation=implementation, base_kwds=base_kwds,
-                                        **kwds)
+                                        variables=variables, nu=nu, dt=dt,
+                                        implementation=implementation, 
+                                        base_kwds=base_kwds, **kwds)
diff --git a/hysop/operator/external_force.py b/hysop/operator/external_force.py
new file mode 100644
index 0000000000000000000000000000000000000000..0709bbfbe17bb743a2cce46336f34a92358116b4
--- /dev/null
+++ b/hysop/operator/external_force.py
@@ -0,0 +1,126 @@
+
+from hysop.constants import Implementation
+from hysop.fields.continuous_field import Field, ScalarField
+from hysop.parameters.tensor_parameter import TensorParameter
+from hysop.parameters.scalar_parameter import ScalarParameter
+from hysop.operator.min_max import MinMaxFieldStatisticsBase
+from hysop.core.graph.computational_node_frontend import ComputationalGraphNodeFrontend
+from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
+from hysop.tools.types import first_not_None, to_tuple, check_instance
+from hysop.tools.sympy_utils import nabla
+from hysop.tools.decorators import debug
+
+
+class SpectralExternalForce(ComputationalGraphNodeFrontend):
+    """
+    Generate an operator to compute the curl of a symbolic expression.
+    """
+
+    @classmethod
+    def implementations(cls):
+        from hysop.backend.device.opencl.operator.external_force import OpenClSpectralExternalForce
+        __implementations = {
+                Implementation.OPENCL: OpenClSpectralExternalForce,
+        }
+        return __implementations
+    
+    @classmethod
+    def default_implementation(cls):
+        return Implementation.OPENCL
+
+    @debug
+    def __init__(self, vorticity, Fext, dt, variables,
+                    Fmin=None, Fmax=None, Finf=None, 
+                    all_quiet=False, pbasename=None, ppbasename=None,
+                    implementation=None, base_kwds=None, **kwds):
+        """
+        Create an operator that computes the curl of a given input force field Fext.
+
+        Only the following configurations are supported:
+                     dim   nb_components  |   dim   nb_components
+        vorticity:    2          1        |    3          3
+
+        What is computed:
+            tmp  = curl(Fext) by using a spectral backend
+            Fmin = min(tmp)
+            Fmax = max(tmp)
+            Finf = max(abs(Fmin), abs(Fmax))
+            W   += dt*tmp
+        
+        where Fext is computed from user given ExternalForce.
+
+        Parameters
+        ----------
+        vorticity: hysop.field.continuous_field.Field
+            Continuous field as input ScalarField or VectorField.
+            All contained field have to live on the same domain.
+        Fext: hysop.operator.external_force.ExternalForce
+            Expression of the external force.
+        dt: ScalarParameter
+            Timestep paramater.
+        F...: ScalarParameter, TensorParameter or boolean, optional
+            TensorParameters should match the shape of tmp (see Notes).
+            If set to True, the TensorParameter will be generated automatically.
+        all_quiet: bool
+            Force all autogenerated TensorParameter to be quiet.
+            By default, only the autogenerated TensorParameters that are not required 
+            by the user are set to be quiet.
+        pbasename: str, optional
+            Parameters basename for created parameters.
+            Defaults to 'curl_{}'.format(Fext.name).
+        ppbasename: str, optional
+            Parameters pretty basename for created parameters.
+            Defaults to '|{} x {}|'.format(nabla, Fext.pretty_name).
+        variables: dict
+            dictionary of fields as keys and topologies as values.
+        implementation: Implementation, optional, defaults to None
+            target implementation, should be contained in available_implementations().
+            If None, implementation will be set to default_implementation().
+        kwds: dict, optional
+            Extra parameters passed towards base class (MultiSpaceDerivatives).
+
+        Notes
+        -----
+        If dim == 2, it is expected that:
+            vorticity has only one component
+            Fext has 2 components
+            Expected parameters are ScalarParameters or TensorParameters of shape (1,)
+        Else if dim == 3:
+            vorticity has 3 components
+            Fext has 3 components
+            Expected parameters are TensorParameter of shape (3,)
+        """
+        from hysop.operator.base.external_force import ExternalForce
+        base_kwds = first_not_None(base_kwds, {})
+        check_instance(vorticity, Field)
+        check_instance(Fext, ExternalForce)
+        check_instance(dt, ScalarParameter)
+        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
+        
+        # Pregenerate parameters so that we can directly store them in self.
+        default_pbasename  = 'curl_{}'.format(Fext.name)
+        default_ppbasename = u'{}x{}'.format(nabla, Fext.pretty_name)
+        pbasename  = first_not_None(pbasename,  default_pbasename)
+        ppbasename = first_not_None(ppbasename, default_ppbasename)
+        parameters = MinMaxFieldStatisticsBase.build_parameters(field=vorticity, 
+                components=None, dtype=None, all_quiet=all_quiet, 
+                Fmin=Fmin, Fmax=Fmax, Finf=Finf, 
+                pbasename=pbasename, ppbasename=ppbasename)
+
+        (Fmin, Fmax, Finf) = tuple(parameters[k] for k in ('Fmin', 'Fmax', 'Finf'))
+        
+        check_instance(Fmin, TensorParameter, allow_none=True)
+        check_instance(Fmax, TensorParameter, allow_none=True)
+        check_instance(Finf, TensorParameter, allow_none=True)
+
+        super(SpectralExternalForce, self).__init__(vorticity=vorticity, 
+                Fext=Fext, dt=dt, variables=variables,
+                Fmin=Fmin, Fmax=Fmax, Finf=Finf,
+                candidate_input_tensors=(vorticity,),
+                candidate_output_tensors=(vorticity,),
+                implementation=implementation, 
+                base_kwds=base_kwds, **kwds)
+
+        self.Fmin = Fmin
+        self.Fmax = Fmax
+        self.Finf = Finf
diff --git a/hysop/operator/gradient.py b/hysop/operator/gradient.py
new file mode 100644
index 0000000000000000000000000000000000000000..5614f5c70f4d8e2f08696f7e3aee4977f9e387d4
--- /dev/null
+++ b/hysop/operator/gradient.py
@@ -0,0 +1,422 @@
+"""
+@file gradient.py
+Gradient: compute dFi/dXj for a given field, up to all components in all directions.
+MinMaxGradientStatistics: compute min(dFi/dXj), max(dFi/dXj) and/or max(|dFi/dXj|) 
+                          for a given field, up to all components in all directions.
+"""
+from hysop import vprint
+from hysop.constants import Implementation, DirectionLabels, TranspositionState
+from hysop.fields.continuous_field import Field, ScalarField, TensorField
+from hysop.tools.types import check_instance, first_not_None, to_tuple
+from hysop.tools.decorators import debug
+from hysop.tools.numpywrappers import npw
+from hysop.core.graph.graph import op_apply
+from hysop.core.graph.computational_operator import ComputationalGraphOperator
+from hysop.core.graph.computational_node import ComputationalGraphNode
+from hysop.core.graph.computational_node_frontend import ComputationalGraphNodeFrontend
+from hysop.core.graph.node_generator import ComputationalGraphNodeGenerator
+from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
+from hysop.operator.directional.directional import DirectionalOperatorGeneratorI
+from hysop.operator.derivative import SpaceDerivative, MultiSpaceDerivatives, \
+        FiniteDifferencesSpaceDerivative, SpectralSpaceDerivative
+from hysop.operator.min_max import MinMaxDerivativeStatistics, \
+        MinMaxFiniteDifferencesDerivativeStatistics, MinMaxSpectralDerivativeStatistics
+from hysop.parameters.scalar_parameter import ScalarParameter
+from hysop.parameters.tensor_parameter import TensorParameter
+
+class Gradient(MultiSpaceDerivatives):
+    """
+    Generate multiple SpaceDerivative operators to compute the gradient of a Field.
+    """
+
+    @classmethod
+    def implementations(cls):
+        return SpaceDerivative.implementations()
+
+    @debug
+    def __init__(self, F, gradF, directions=None, implementation=None, 
+                        cls=FiniteDifferencesSpaceDerivative, 
+                        base_kwds=None, **kwds):
+        """
+        Create an operator generator that yields a sequence of operators
+        that compute the gradient of an input field F.
+
+        Given F, a scalar, vector or tensor field of dimension n, 
+        compute the field of dimension n+1 that is the gradient of F:
+            ScalarField: F -> gradF[j]   = dF/dxj
+            VectorField: F -> gradF[i,j] = dFi/dxj
+            TensorField: F -> gradF[i0,...,in,j] = dF[i0,...,in]/dxj
+        
+        Derivatives can be computed with respect to specific directions and not necessarily 
+        in all directions.
+        To restrict the number of components, take a tensor view on F (and gradF).
+
+        Example: if F is a VectorField of m components (F0, ..., Fm) in a domain of dimension n,
+                 this operator will compute gradF[i,j] = dF[i]/dx[j].
+                
+                    ================================
+                               dF0/dx0 ... dF0/dxn
+                                  .     .     .
+                    grad(F) =     .     .     .
+                                  .     .     .
+                               dFm/dx0 ... dFm/dxn
+                    ================================
+
+                    where F is an input field
+                          gradF is an output field != F
+                          0 <= i < nb_components
+                          0 <= j < nb_directions
+                          nb_components = F.nb_components
+                          nb_directions = min( F.dim, len(directions))
+
+        Parameters
+        ----------
+        F: hysop.field.continuous_field.Field
+            Continuous field as input (Scalar, Vector or TensorField).
+            All contained field have to live on the same domain.
+        gradF: hysop.field.continuous_field.Field
+            Continuous field to be written, should have
+            exactly shape F.shape + (ndirections,)
+        directions: tuple of ints, optional
+            The directions in which to take the derivatives.
+            Defaults to range(F.dim).
+            nb_directions = min(F.dim, len(directions))
+        implementation: Implementation, optional, defaults to None
+            target implementation, should be contained in available_implementations().
+            If None, implementation will be set to default_implementation().
+        kwds: dict, optional
+            Extra parameters passed towards base class (MultiSpaceDerivatives).
+        """
+        base_kwds = first_not_None(base_kwds, {})
+        check_instance(F, Field)
+        check_instance(gradF, Field)
+        check_instance(directions, tuple, values=int, allow_none=True)
+
+        directions = to_tuple(first_not_None(directions, range(F.dim)))
+        ndirections = len(directions)
+
+        if F.is_tensor:
+            nfields = F.size
+            oshape = F.shape + (ndirections,)
+        else:
+            nfields = 1
+            oshape = (ndirections,)
+        
+        if (gradF.is_tensor):
+            if (gradF.shape != oshape):
+                msg='Gradient field shape mismatch, expected {} but got {}.'
+                msg=msg.format(oshape, gradF.shape)
+                raise ValueError(msg)
+        else:
+            if (oshape != (1,)):
+                msg='Gradient field shape mismatch, expected {} but got a scalar '
+                msg+='field (ie. shape={}).'
+                msg=msg.format(oshape, (1,))
+                raise ValueError(msg)
+
+        Fs  = tuple(f for f in F.fields for d in directions)
+        dFs = gradF.fields
+        directions  = tuple(d for _ in xrange(nfields) for d in directions)
+        derivatives = (1,)*len(directions)
+        
+        base_kwds.update(dict(
+                candidate_input_tensors=(F,),
+                candidate_output_tensors=(gradF,)))
+        
+        if not issubclass(cls, (SpaceDerivative, MinMaxDerivativeStatistics)) or \
+                (cls in (SpaceDerivative, MinMaxDerivativeStatistics)):
+            msg="cls should be a subclass of SpaceDerivative or MinMaxSpaceDerivativeStatistics, got {}."
+            msg+='\ncls MRO is:\n  '
+            msg+='\n  '.join(str(t) for t in cls.__mro__)
+            msg=msg.format(cls)
+            raise TypeError(msg)
+
+        implementation = first_not_None(implementation, cls.default_implementation())
+        super(Gradient, self).__init__(Fs=Fs, dFs=dFs, cls=cls,
+                candidate_input_tensors=(F,),
+                candidate_output_tensors=(gradF,),
+                derivatives=derivatives, directions=directions, 
+                implementation=implementation, 
+                base_kwds=base_kwds, **kwds)
+
+
+class MinMaxGradientStatistics(Gradient):
+    """
+    Interface for computing some statistics on the gradient of a field (minimum and maximum)
+    one component at a time to limit memory usage.
+    This will generate multiple MinMaxDerivativeStatistics operators.
+    """
+    
+    @debug
+    def __init__(self, F, gradF=None, directions=None, coeffs=None,
+            Fmin=None, Fmax=None, Finf=None, 
+            all_quiet=True, print_tensors=True,
+            name=None, pretty_name=None, pbasename=None, ppbasename=None,
+            variables=None, implementation=None, base_kwds=None, 
+            cls=MinMaxFiniteDifferencesDerivativeStatistics,
+            **kwds):
+        """
+        Create an operator generator that yields a sequence of operators
+        that compute statistics on the gradient of an input field F.
+        
+        MinMaxGradientStatistics can compute some commonly used Field statistics:
+            Fmin:  component-wise and direction-wise min values of the gradient of the field.
+            Fmax:  component-wise and direction-wise max values of the gradient of the field.
+            Finf:  component-wise and direction-wise max values of the absolute value of the 
+                        gradient of the field (computed using Fmin and Fmax).
+
+        Derivatives can be computed with respect to specific directions and not necessarily in 
+        all directions. To restrict the number of components, take a tensor view on F (and gradF).
+        
+        ----------------------------------------------
+        Let k = idx + (j,)
+        gradF[k] = dF[idx]/dXd
+        ----------------------------------------------
+        Fmax[k]  = Smin * min( dF[idx]/dXd )
+        Fmin[k]  = Smax * max( dF[idx]/dXd )
+        Finf[k]  = Sinf * max( |Fmin[k]|, |Fmax[k]| )
+        ----------------------------------------------
+        where F is an input field,
+              nb_components = F.nb_components = np.prod(F.shape)
+              nb_directions = min( F.dim, len(directions))
+              gradF is an optional output tensor field,
+              idx is contained in numpy.ndindex(*F.shape)
+              0 <= j < nb_directions
+              d = directions[j]
+              Fmin = created or supplied TensorParameter of shape F.shape + (nb_directions,).
+              Fmax = created or supplied TensorParameter of shape F.shape + (nb_directions,).
+              Finf = created or supplied TensorParameter of shape F.shape + (nb_directions,).
+              Smin = coeffs['Fmin'] or coeffs['Fmin'][k]
+              Smax = coeffs['Fmax'] or coeffs['Fmax'][k]
+              Sinf = coeffs['Finf'] or coeffs['Fmax'][k]
+
+        All statistics are only computed if explicitely required by user,
+          unless required to compute another required statistic, see Notes.
+        
+        Parameters
+        ----------
+        F: Field
+            The continuous input field on which the gradient
+            will be taken and statistics will be computed.
+        gradF: Field, optional
+            Optional output field for the gradient.
+            If the gradient is required as an output, one can also use MinMaxStatistics
+            on a precomputed gradient (using the Gradient operator) instead of 
+            MinMaxGradientStatistics.
+        directions: array like of ints, optional
+            The directions in which the statistics are computed,
+            defaults to all directions (ie. range(F.dim)).
+        coeffs: dict of scalar or array like of coefficients, optional
+            Optional scaling of the statistics.
+            Scaling factor should be a scalar or an array-like of scalars for
+            each components of gradF. If not given default to 1 for all statistics.
+        F...: TensorParameter or boolean, optional
+            At least one statistic should be specified (either by boolean or TensorParameter).
+            TensorParameters should be of shape F.shape + (nb_directions,), see Notes.
+            If set to True, the TensorParameter will be generated automatically.
+            Autogenerated TensorParameters that are not required by the user
+            but are generated anyway are set to be quiet.
+            All autogenerated TensorParameters can be retrieved as attributes
+            of this object.
+        all_quiet: bool, optional, defaults to True
+            Set all generated params to be quiet, even the ones that are requested
+            explicitely. 
+        print_tensors: bool, optional, defaults to True
+            Should the phony operator print the tensor parameters during apply ?
+        name: str, optional
+            Name template for generated operator names.
+            Defaults to MinMax({}) where {} will be replaced by gradF[k].name.
+        pretty_name: str, optional
+            Name template for generaed operatr pretty names.
+            Defaults to |+/-{}| where {} will be replaced by gradF[k].pretty_name.
+        pbasename: str, optional
+            Basename for created tensor parameters.
+            Defaults to gradF.name.
+        ppbasename: str, optional
+            Pretty basename for created tensor parameters.
+            Defaults to gradF.pretty_name.
+        variables: dict
+            Dictionary of fields as keys and topologies as values.
+        implementation: hysop.constants.Implementation, optional
+            Specify generated operator underlying backend implementation.
+            Target implementation, should be contained in 
+              MinMaxDerivativeStatistics.available_implementations().
+            If None, implementation will be set to 
+              MinMaxDerivativeStatistics.default_implementation().
+        base_kwds: dict
+            Base class keyword arguments.
+        kwds: dict
+            Additional kwds passed to chosen implementation.
+
+        Attributes:
+        -----------
+        Fmin, Fmax, Finf: TensorParameter
+            All generated tensor parameters.
+            Unused statistics are set to None.
+
+        Notes
+        -----
+        nb_components = F.nb_components
+        nb_directions = min(F.dim, len(directions)).
+        
+        About statistics:
+            Finf requires to compute Fmin and Fmax.
+                Finf = Sinf * max( abs(Smin*Fmin), abs(Smax*Fmax))
+            where Sinf, Smin and Smax are the scaling coefficients defined in coeffs.
+        """
+
+        check_instance(F, Field)
+        check_instance(gradF, Field, allow_none=True)
+        check_instance(directions, tuple, values=int, allow_none=True, 
+                minval=0, maxval=F.dim-1, minsize=1, unique=True)
+        check_instance(coeffs, dict, keys=str, values=(int, float, npw.number), allow_none=True)
+        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors, 
+                        allow_none=True)
+        check_instance(name, str, allow_none=True)
+        check_instance(pbasename, str, allow_none=True)
+        check_instance(ppbasename, (str,unicode), allow_none=True)
+        check_instance(implementation, Implementation, allow_none=True)
+        check_instance(base_kwds, dict, allow_none=True)
+        check_instance(all_quiet, bool, allow_none=True)
+
+        if (    ((Fmin is None) or (Fmin is False)) 
+            and ((Fmax is None) or (Fmax is False))
+            and ((Finf is None) or (Finf is False))):
+            msg='No statistics were requested.'
+            msg+='\nPlease specify Fmin, Fmax and/or Finf by either setting '
+            msg+=' their value to True, or by by passing an already existing '
+            msg+=' tensor parameter.'
+            raise ValueError(msg)
+        
+        coeffs     = first_not_None(coeffs, {})
+        variables  = first_not_None(variables, {F: None})
+        all_quiet  = first_not_None(all_quiet, False)
+        
+        directions = to_tuple(first_not_None(directions, range(F.dim)))
+        nb_directions = len(directions)
+        
+        if F.is_tensor:
+            oshape = F.shape + (nb_directions,)
+        else:
+            oshape = (nb_directions,)
+        
+        if (gradF is None):
+            gradF = F.gradient(directions=directions, is_tmp=True)
+        assert (gradF.shape == oshape), gradF.shape
+        
+        variables.setdefault(gradF, variables[F])
+        
+        _names = {
+            'Fmin': '{}_min',
+            'Fmax': '{}_max',
+            'Finf': '{}_inf'
+        }
+
+        _pretty_names = {
+            'Fmin': u'{}\u208b',
+            'Fmax': u'{}\u208a',
+            'Finf': u'|{}|\u208a'
+        }
+
+        pbasename  = first_not_None(pbasename, gradF.name)
+        ppbasename = first_not_None(ppbasename, gradF.pretty_name)
+        
+        names = { k: v.format(pbasename) for (k,v) in _names.iteritems() }
+        pretty_names = { k: v.format(ppbasename.decode('utf-8')) 
+                            for (k,v) in _pretty_names.iteritems() }
+
+        def make_param(k, quiet):
+            return TensorParameter(name=names[k], pretty_name=pretty_names[k],
+                    dtype=F.dtype, shape=oshape, quiet=quiet)
+
+        parameters = {}
+        _parameters = dict(Fmin=Fmin, Fmax=Fmax, Finf=Finf)
+        for (k,v) in _parameters.iteritems():
+            param = _parameters[k]
+            if isinstance(param, TensorParameter):
+                pass
+            elif (param is True):
+                param = make_param(k, quiet=all_quiet)
+            elif (_parameters['Finf'] is not None) and ((k == 'Fmin') or (k == 'Fmax')):
+                param = make_param(k, quiet=True)
+            else:
+                param = None
+                setattr(self, k, param)
+                continue
+            assert param.shape == oshape
+            coeffs.setdefault(k, 1)
+            parameters[k] = param
+            setattr(self, k, param)
+
+        unused_coeffs = set(coeffs.keys()) - set(parameters.keys())
+        if unused_coeffs:
+            msg='The following coefficients are not needed: {}'
+            msg=msg.format(unused_coeffs)
+            raise ValueError(unused_coeffs)
+        
+        name = first_not_None(name, 'MinMax({})')
+        pretty_name = first_not_None(pretty_name, u'|\u00b1{}|')
+
+        extra_params = { 'name': gradF.new_empty_array(), 
+                         'pretty_name': gradF.new_empty_array(), 
+                         'coeffs': coeffs, 
+                         'implementation': implementation }
+
+        for (idx, Fi) in gradF.nd_iter():
+            for (statname, stat) in parameters.iteritems():
+                if (stat is None):
+                    continue
+                pname  = _names[statname].format(Fi.name)
+                ppname = _pretty_names[statname].format(Fi.pretty_name.decode('utf-8'))
+                S = stat.view(idx=idx, name=pname, pretty_name=ppname)
+                stats = extra_params.setdefault(statname, gradF.new_empty_array())
+                stats[idx] = S
+            extra_params['name'][idx] = name.format(Fi.name)
+            extra_params['pretty_name'][idx] = pretty_name.format(
+                                                Fi.pretty_name.decode('utf-8')).encode('utf-8')
+
+        super(MinMaxGradientStatistics, self).__init__(F=F, gradF=gradF,
+                directions=directions, extra_params=extra_params,
+                cls=cls, variables=variables, **kwds)
+
+        # add a phony operator to gather parameter views
+        class MergeTensorViewsOperator(ComputationalGraphOperator):
+            @op_apply
+            def apply(self, **kwds):
+                super(MergeTensorViewsOperator, self).apply(**kwds)
+                if not print_tensors:
+                    return
+                for (k,v) in _parameters.iteritems():
+                    if (v is not None) and (v is not False):
+                        param = parameters[k]
+                        msg='>Parameter {} set to:\n{}'.format(param.pretty_name, param.value)
+                        vprint(msg)
+
+
+        _phony_input_params  = {}
+        _phony_output_params = {}
+        for pname in _names.keys():
+            if (pname in extra_params):
+                param = parameters[pname]
+                _phony_input_params.update({p.name:p for p in extra_params[pname].ravel()})
+                _phony_output_params[param.name] = param
+        op = MergeTensorViewsOperator(name=name.format(gradF.name), 
+                pretty_name=pretty_name.format(gradF.pretty_name.decode('utf-8')),
+                input_params=_phony_input_params, 
+                output_params=_phony_output_params)
+        self._phony_op = op
+
+    @debug
+    def _generate(self):
+        """Generate all operators."""
+        operators = super(MinMaxGradientStatistics, self)._generate()
+        operators += (self._phony_op,)
+        return operators
+    
+    @debug
+    def generate_direction(self, i, dt_coeff):
+        # See MultiSpaceDerivatives for the directional interface
+        ops = super(MinMaxGradientStatistics, self).generate_direction(i=i, dt_coeff=dt_coeff)
+        if ops and (i==max(self.directions)):
+            ops += (self._phony_op,)
+        return ops
diff --git a/hysop/operator/hdf_io.py b/hysop/operator/hdf_io.py
index 6eed6a210983e011c66f9cca81dc6b2c65e8dbb5..dc6b5926612800b647186be6eb215c06b2b3bde6 100755
--- a/hysop/operator/hdf_io.py
+++ b/hysop/operator/hdf_io.py
@@ -7,6 +7,7 @@
 * :class:`~HDF_IO` abstract interface for hdf io classes
 
 """
+import functools
 from abc import ABCMeta, abstractmethod
 from hysop.deps import h5py, sys
 from hysop.core.graph.graph import discretized
@@ -19,7 +20,8 @@ from hysop.core.graph.graph import op_apply
 from hysop.core.graph.computational_graph import ComputationalGraphOperator
 from hysop.fields.continuous_field import Field
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
-from hysop.mesh.subsets import Subset
+from hysop.core.memory.memory_request import MemoryRequest
+from hysop.topology.topology_descriptor import TopologyDescriptor
 
 class HDF_IO(ComputationalGraphOperator):
     """
@@ -28,7 +30,7 @@ class HDF_IO(ComputationalGraphOperator):
     """
 
     __metaclass__ = ABCMeta
-
+    
     @classmethod
     def supported_backends(cls):
         """
@@ -36,9 +38,9 @@ class HDF_IO(ComputationalGraphOperator):
         """
         return Backend.all
 
-    def __init__(self, var_names=None,
-                name_prefix='', name_postfix='',
-                subset=None, **kwds):
+    def __init__(self, var_names=None, 
+                name_prefix='', name_postfix='', 
+                force_backend=None, **kwds):
         """Read/write some fields data from/into hdf/xmdf files.
         Parallel io.
 
@@ -51,9 +53,8 @@ class HDF_IO(ComputationalGraphOperator):
             Optional name prefix for variables.
         name_postfix: str, optional
             Optional name postfix for variables.
-        subset : :class:`~hysop.domain.subset.Subset`, optional
-            a subset of the domain, on which data are read or written,
-            default=the whole domain.
+        force_backend: hysop.constants.Backend
+            Force the source backend for fields.
         kwds: dict
             Base class arguments.
 
@@ -72,7 +73,6 @@ class HDF_IO(ComputationalGraphOperator):
         """
 
         check_instance(var_names, dict, keys=Field, values=str, allow_none=True)
-        check_instance(subset, Subset, allow_none=True)
 
         super(HDF_IO, self).__init__(**kwds)
 
@@ -103,8 +103,6 @@ class HDF_IO(ComputationalGraphOperator):
             self.io_params = IOParams(name, fileformat=IO.HDF5)
         else:
             assert self.io_params.fileformat is IO.HDF5
-        # Set a subset of the original domain
-        self.subset = subset
 
         # Dictionnary of names to search in hdf file. May be None.
         # It will be checked during setup.
@@ -113,7 +111,7 @@ class HDF_IO(ComputationalGraphOperator):
         # Local topology, that MUST be common to all input_fields.
         self.topology = None
         self._local_compute_slices = None
-        self._global_resolution = None
+        self._global_grid_resolution = None
         self._global_slices = None
         # Dictionnary of discrete fields. Key = name in hdf file,
         # Value = discrete field
@@ -123,7 +121,40 @@ class HDF_IO(ComputationalGraphOperator):
         self._get_filename = lambda i=None: None
         # File Object that holds hdf file
         self._hdf_file = None
-
+        # field backend
+        self._force_backend = first_not_None(force_backend, Backend.HOST)
+        td_kwds = {}
+        if (force_backend is Backend.OPENCL):
+            assert 'cl_env' in kwds
+            td_kwds['cl_env'] = kwds.pop('cl_env')
+        self._td_kwds = td_kwds
+    
+    @debug
+    def create_topology_descriptors(self):
+        """
+        Called in get_field_requirements, just after handle_method
+         Topology requirements (or descriptors) are:
+             1) min and max ghosts for each input and output variables
+             2) allowed splitting directions for cartesian topologies
+        """
+        # by default we create HOST (cpu) TopologyDescriptors
+        td_kwds = self._td_kwds
+        for (field, topo_descriptor) in self.input_fields.iteritems():
+            topo_descriptor = TopologyDescriptor.build_descriptor(
+                    backend=self._force_backend,
+                    operator=self,
+                    field=field,
+                    handle=topo_descriptor, **td_kwds)
+            self.input_fields[field] = topo_descriptor
+
+        for (field, topo_descriptor) in self.output_fields.iteritems():
+            topo_descriptor = TopologyDescriptor.build_descriptor(
+                    backend=self._force_backend,
+                    operator=self,
+                    field=field,
+                    handle=topo_descriptor, **td_kwds)
+            self.output_fields[field] = topo_descriptor
+    
     @debug
     def get_field_requirements(self):
         # set good transposition state
@@ -134,23 +165,24 @@ class HDF_IO(ComputationalGraphOperator):
             (field, td, req) = ireq
             req.axes = (TranspositionState[field.dim].default_axes(),)
         return requirements
+    
+    def get_node_requirements(self):
+        node_reqs = super(HDF_IO, self).get_node_requirements()
+        node_reqs.enforce_unique_transposition_state = True
+        node_reqs.enforce_unique_topology_shape      = True
+        node_reqs.enforce_unique_memory_order        = False
+        node_reqs.enforce_unique_ghosts              = False
+        return node_reqs
 
     def discretize(self):
         super(HDF_IO, self).discretize()
         self.topology = self.input_fields.values()[0]
-        # Discretize the subset, if required
-        if (self.subset is not None):
-            raise NotImplementedError
-            self.subset.discretize(self.topology)
-            refmesh = self.subset.mesh[self.topology]
-        else:
-            refmesh = self.topology.mesh
-        self.refmesh = refmesh
 
-        # Global resolution for hdf5 output (warning : this must
-        # be the whole domain resolution, not the subset resolution)
-        self._global_resolution = refmesh.grid_resolution
+        refmesh = self.topology.mesh
 
+        # Global resolution for hdf5 output
+        self._global_grid_resolution = refmesh.grid_resolution
+        
         local_compute_slices = {}
         global_compute_slices = {}
         for (field, itopo) in self.input_fields.iteritems():
@@ -162,29 +194,23 @@ class HDF_IO(ComputationalGraphOperator):
                 local_compute_slices[field]  = mesh.local_compute_slices
                 global_compute_slices[field] = mesh.global_compute_slices
             else:
-                local_compute_slices[field] = tuple(slice(0, 0) for _ in xrange(self.domain.dim))
+                local_compute_slices[field]  = tuple(slice(0, 0) for _ in xrange(self.domain.dim))
                 global_compute_slices[field] = tuple(slice(0, 0) for _ in xrange(self.domain.dim))
         self._local_compute_slices = local_compute_slices
         self._global_compute_slices = global_compute_slices
+        self.refmesh = refmesh
 
-    def setup(self, work=None):
-        super(HDF_IO, self).setup(work=work)
-        # No list of hdf dataset names provided by user ...
+    #def setup(self, work=None):
+        #super(HDF_IO, self).setup(work=work)
+         #No list of hdf dataset names provided by user ...
 
         name_prefix, name_postfix = self.name_prefix, self.name_postfix
-        idtf = self.input_discrete_tensor_fields
         if (self.var_names is None):
             var_names = {}
             # Get field names and initialize dataset dict.
             for df in self.discrete_fields:
                 for d in xrange(df.nb_components):
-                    name = name_prefix
-                    if len(idtf)>0 and df.field in idtf.keys()[0].fields:
-                        tf = [_ for _ in idtf.keys() if df.field in _.fields][0]
-                        name += tf.name + '_' + DirectionLabels[tf.fields.index(df.field)]
-                    else:
-                        name += df.name + '_' + DirectionLabels[d]
-                    name += name_postfix
+                    name = name_prefix + df.name + '_' + DirectionLabels[d] + name_postfix
                     self.dataset[name] = df.data[d]
                     var_names[df.field] = name
             self.var_names = var_names
@@ -193,7 +219,7 @@ class HDF_IO(ComputationalGraphOperator):
                 # Discrete field associated to var
                 var_d = var.discretize(self.topology)
                 for d in xrange(var_d.nb_components):
-                    name = name_prefix + self.var_names[var]
+                    name = name_prefix + self.var_names[var] 
                     name += '_' + DirectionLabels[d] + name_postfix
                     self.dataset[name] = var_d.data[d]
 
@@ -217,7 +243,7 @@ class HDF_IO(ComputationalGraphOperator):
 
     @classmethod
     def supports_multiple_topologies(cls):
-        return True
+        return True 
     @classmethod
     def supports_mpi(cls):
         return True
@@ -226,7 +252,7 @@ class HDF_Writer(HDF_IO):
     """
     Print field(s) values on a given topo, in HDF5 format.
     """
-    def __init__(self, variables, xmfalways=True,
+    def __init__(self, variables, xmfalways=True, 
             name=None, pretty_name=None, **kwds):
         """
         Write some fields data into hdf/xmdf files.
@@ -240,9 +266,9 @@ class HDF_Writer(HDF_IO):
             default=True
         kwds : base class arguments
         """
-
+        
         check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
-
+        
         vnames = ['{}'.format(field.name) for field in variables.keys()]
         vpnames = [field.pretty_name.decode('utf-8') for field in variables.keys()]
         name   = first_not_None(name, 'write_{}'.format('_'.join(vnames)))
@@ -266,10 +292,60 @@ class HDF_Writer(HDF_IO):
         # if that happens.
         self._last_written_time = None
         self._xmf_file = None
+        self._data_getters = {}
+
+    def get_work_properties(self, **kwds):
+        requests = super(HDF_Writer, self).get_work_properties(**kwds)
 
-    def setup(self, **kwds):
-        super(HDF_Writer,self).setup(**kwds)
+        max_bytes = 0
+        for (name, data) in self.dataset.iteritems():
+            if (data.backend.kind == Backend.HOST):
+                continue
+            if (data.backend.kind == Backend.OPENCL):
+                from hysop.backend.device.opencl import cl
+                if (data.backend.device.type == cl.device_type.CPU):
+                    continue
+            # we need a host buffer to get the data
+            max_bytes = max(data.nbytes, max_bytes)
+            host_backend = data.backend.host_array_backend
+        
+        if (max_bytes > 0):
+            request = MemoryRequest(backend=host_backend, size=max_bytes, dtype=npw.uint8)
+            requests.push_mem_request(request_identifier='buffer', mem_request=request)
+
+        return requests
+
+    def setup(self, work, **kwds):
+        super(HDF_Writer, self).setup(work=work, **kwds)
         self._setup_grid_template()
+        for (name, data) in self.dataset.iteritems():
+            data = data[self._local_compute_slices[name]]
+            if (data.backend.kind is Backend.HOST):
+                def get_data(data=data.handle):
+                    return data
+            elif (data.backend.kind is Backend.OPENCL):
+                from hysop.backend.device.opencl.opencl_copy_kernel_launchers import OpenClCopyBufferRectLauncher
+                from hysop.backend.device.opencl import cl
+                if (data.backend.device.type == cl.device_type.CPU):
+                    def get_data(data=data.handle, queue=data.backend.cl_env.default_queue): 
+                        buf = data.map_to_host(queue=queue,
+                                is_blocking=True, flags=cl.map_flags.READ)
+                        return buf
+                        # unmap is called when buf is destroyed
+                else:
+                    buf, = work.get_buffer(self, 'buffer', handle=True) 
+                    assert buf.dtype == npw.uint8
+                    assert buf.size >= data.nbytes
+                    buf = buf[:data.nbytes].view(dtype=data.dtype).reshape(data.shape)
+                    cpy = OpenClCopyBufferRectLauncher.from_slices(varname=name, src=data, dst=buf)
+                    cpy = functools.partial(cpy, queue=data.backend.cl_env.default_queue)
+                    def get_data(cpy=cpy, buf=buf):
+                        cpy().wait()
+                        return buf
+            else:
+                msg='Data type not understood or unknown array backend.'
+                raise NotImplementedError(msg)
+            self._data_getters[name] = get_data
 
     def finalize(self):
         if self._xmf_file:
@@ -300,13 +376,8 @@ class HDF_Writer(HDF_IO):
         dim = topo.domain.dim
         dx = list(topo.mesh.space_step)
         mesh = self.refmesh
-        subset = self.subset
-        if (subset is not None):
-            res  = list(mesh[topo].grid_resolution)
-            orig = list(subset.real_orig[topo])
-        else:
-            res  = list(mesh.grid_resolution)
-            orig = list(topo.domain.origin)
+        res  = list(mesh.grid_resolution)
+        orig = list(topo.domain.origin)
         resolution = [1,]*3
         origin     = [0.0,]*3
         step = [0.0,]*3
@@ -319,7 +390,7 @@ class HDF_Writer(HDF_IO):
         write_resolution = tuple(resolution)
         write_origin = tuple(origin)
         write_step = tuple(step)
-
+        
         ds_names = self.dataset.keys()
         grid_attributes = XMF.prepare_grid_attributes(
                             ds_names,
@@ -379,13 +450,12 @@ class HDF_Writer(HDF_IO):
         # datasets
         for name in self.dataset:
             ds = self._hdf_file.create_dataset(name,
-                                               self._global_resolution,
-                                               dtype=HYSOP_REAL,
+                                               self._global_grid_resolution,
+                                               dtype=npw.float64,
                                                compression=compression)
             # In parallel, each proc must write at the right place of the dataset
-            data = self.dataset[name].get()
-            ds[self._global_compute_slices[name]] = npw.asrealarray(data[self._local_compute_slices[name]])
-
+            ds[self._global_compute_slices[name]] = self._data_getters[name]()
+        
         # Collect datas required to write the xdmf file
         # --> add tuples (counter, time).
         if (simu.t() == self._last_written_time):
@@ -403,6 +473,7 @@ class HDF_Writer(HDF_IO):
         self._step_HDF5(simu)
         self.updateXMFFile()
 
+
 class HDF_Reader(HDF_IO):
     """
     Parallel reading of hdf/xdmf files to fill some fields in.
@@ -424,10 +495,10 @@ class HDF_Reader(HDF_IO):
         See examples in tests_hdf_io.py
         """
 
-        vnames = ['{}[{}]'.format(var.name[:3], topo.id)
+        vnames = ['{}[{}]'.format(var.name[:3], topo.id) 
                     for var,topo in variables.iteritems()]
         name = name or 'read_{}'.format(','.join(vnames))
-        super(HDF_Reader, self).__init__(input_fields=None, output_fields=variables,
+        super(HDF_Reader, self).__init__(input_fields=None, output_fields=variables, 
                                             name=name, **kwds)
         self.restart = restart
         if self.restart is not None:
@@ -436,7 +507,7 @@ class HDF_Reader(HDF_IO):
                 self.io_params.filename + "_{0:05d}".format(i) + '.h5'
         else:
             self._get_filename = lambda i=None: self.io_params.filename
-
+    
     @op_apply
     def apply(self, simulation=None, **kwds):
         # Read HDF file
diff --git a/hysop/operator/mean_field.py b/hysop/operator/mean_field.py
index 7c7756cf12cdeb8ff12d3a22cc4c994e48991435..ad203012eea748e50995b8d0ae7dc2423489219b 100644
--- a/hysop/operator/mean_field.py
+++ b/hysop/operator/mean_field.py
@@ -115,6 +115,7 @@ class ComputeMeanField(ComputationalGraphOperator):
     def apply(self, simulation, **kwds):
         if (simulation is None):
             raise ValueError("Missing simulation value for monitoring.")
+        ite = simulation.current_iteration
         should_dump  = (self.io_params.frequency>0) and (ite % self.io_params.frequency == 0)
         should_dump |= simulation.is_time_of_interest
         if should_dump:
diff --git a/hysop/operator/min_max.py b/hysop/operator/min_max.py
index c1c183da528808c24e94c0fa0d29c17705e68b31..c9533bbb1e13c92a3d32aabca7d53395ff8a0753 100644
--- a/hysop/operator/min_max.py
+++ b/hysop/operator/min_max.py
@@ -1,8 +1,10 @@
 """
 @file min_max.py
-MinMaxFieldStatistics:      compute min(f), max(f) and/or max(|f|) for a given field f, component-wise. 
-MinMaxDerivativeStatistics: compute min(d^k(Fi)/dXj^k), max(d^kFi/dXj^k) and/or max(|dFi/dXj|) for a given field, component, direction and order.
-MinMaxGradientStatistics:   compute min(dFi/dXj), max(dFi/dXj) and/or max(|dFi/dXj|) for a given field, up to all components in all directions.
+MinMaxFieldStatistics: compute min(f), max(f) and/or max(|f|) for a given field f.
+MinMaxDerivativeStatistics: compute min(d^k(Fi)/dXj^k), max(d^kFi/dXj^k) and/or max(|dFi/dXj|)
+                            for a given field, component, direction and order.
+MinMaxGradientStatistics: compute min(dFi/dXj), max(dFi/dXj) and/or max(|dFi/dXj|) 
+                          for a given field, up to all components in all directions.
 """
 from hysop import vprint
 from hysop.constants import Backend, Implementation
@@ -18,10 +20,8 @@ from hysop.parameters.tensor_parameter import TensorParameter
 from hysop.core.graph.computational_operator import ComputationalGraphOperator
 from hysop.core.graph.computational_node_frontend import ComputationalGraphNodeFrontend
 from hysop.core.graph.graph import op_apply
-from hysop.operator.derivative import Gradient
 from hysop.operator.base.min_max import MinMaxFieldStatisticsBase, MinMaxDerivativeStatisticsBase
 
-
 class MinMaxFieldStatistics(ComputationalGraphNodeFrontend):
     """
     Operator frontend to compute min and max statistics on the specific field.
@@ -53,7 +53,8 @@ class MinMaxFieldStatistics(ComputationalGraphNodeFrontend):
         MinMaxFieldStatistics can compute some commonly required Field statistics:
             Fmin:  component-wise min values of the field.
             Fmax:  component-wise max values of the field.
-            Finf:  component-wise max values of the absolute value of the field (computed using Fmin and Fmax).
+            Finf:  component-wise max values of the absolute value of the field (computed using
+                                                                                 Fmin and Fmax).
 
         All statistics are only computed if explicitely requested by user,
           unless required to compute another user-required statistic, see Notes.
@@ -170,19 +171,14 @@ class MinMaxDerivativeStatistics(ComputationalGraphNodeFrontend):
     Operator frontend to compute min and max statistics on a specific 
     derivative of a field component, without keeping its output.
     """
+    
     @classmethod
     def implementations(cls):
-        from hysop.backend.host.python.operator.min_max import PythonMinMaxDerivativeStatistics
-        from hysop.backend.device.opencl.operator.min_max import OpenClMinMaxDerivativeStatistics
-        implementations = {
-                Implementation.PYTHON:         PythonMinMaxDerivativeStatistics,
-                Implementation.OPENCL: OpenClMinMaxDerivativeStatistics
-        }
-        return implementations
-    
+        raise NotImplementedError
+
     @classmethod
     def default_implementation(cls):
-        return Implementation.PYTHON
+        raise NotImplementedError
     
     @debug
     def __init__(self, F, dF=None, A=None, 
@@ -202,8 +198,8 @@ class MinMaxDerivativeStatistics(ComputationalGraphNodeFrontend):
                     derivative of the field (computed using Fmin and Fmax).
         
         First compute the derivative of a component of a field F in a given direction
-        at a given order and on a given backend out of place in a specific output component of dF. 
-        The derivative is then possibly scaled by another field/parameter/value A.
+        at a given order and on a given backend out of place in a specific output component of 
+        dF. The derivative is then possibly scaled by another field/parameter/value A.
 
         After the scaled derivative has been computed, compute user requested statistics
         (min and max values) on this new field and scale those statistics by other scaling 
@@ -323,7 +319,8 @@ class MinMaxDerivativeStatistics(ComputationalGraphNodeFrontend):
         check_instance(direction, int, allow_none=True)
         check_instance(out_component, int, allow_none=True)
         check_instance(coeffs, dict, keys=str, values=(int, float, npw.number), allow_none=True)
-        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors, allow_none=True)
+        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors,
+                        allow_none=True)
         check_instance(name, str, allow_none=True)
         check_instance(pbasename, str, allow_none=True)
         check_instance(ppbasename, (str, unicode), allow_none=True)
@@ -357,288 +354,46 @@ class MinMaxDerivativeStatistics(ComputationalGraphNodeFrontend):
         self.Fmin, self.Fmax, self.Finf = (Fmin, Fmax, Finf)
 
 
-class MinMaxGradientStatistics(Gradient):
+class MinMaxSpectralDerivativeStatistics(MinMaxDerivativeStatistics):
     """
-    Interface for computing some statistics on the gradient of a field (minimum and maximum)
-    one component at a time to limit memory usage.
-    Generate multiple MinMaxDerivativeStatistics operators.
+    Operator frontend to compute min and max statistics on a specific 
+    derivative of a field component using the spectral method.
     """
-    
-    @debug
-    def __init__(self, F, gradF=None, directions=None, coeffs=None,
-            Fmin=None, Fmax=None, Finf=None, 
-            all_quiet=True, print_tensors=True,
-            name=None, pretty_name=None, pbasename=None, ppbasename=None,
-            variables=None, implementation=None, base_kwds=None, **kwds):
-        """
-        Create an operator generator that yields a sequence of operators
-        that compute statistics on the gradient of an input field F.
-        
-        MinMaxGradientStatistics can compute some commonly used Field statistics:
-            Fmin:  component-wise and direction-wise min values of the gradient of the field.
-            Fmax:  component-wise and direction-wise max values of the gradient of the field.
-            Finf:  component-wise and direction-wise max values of the absolute value of the 
-                        gradient of the field (computed using Fmin and Fmax).
-
-        Derivatives can be computed with respect to specific directions and not necessarily in all directions.
-        To restrict the number of components, take a tensor view on F (and gradF).
-        
-        ==============================================================================================================
-                   dF0/dX0 ... dF0/dXn                             0    ...    n
-                      .     .     .                                .     .     .
-        grad(F) =     .     .     .     are mapped to components   .     .     .          in the output statistics.
-                      .     .     .                                .     .     .             n = nb_directions - 1
-                   dFm/dX0 ... dFm/dXn                          m(n+1)   .  (m+1)(n+1)-1     m = F.nb_components - 1
-        ==============================================================================================================
-        
-        Let k = idx + (j,)
-            --------------------
-            gradF[k] = dF[idx]/dXd
-            --------------------
-            Fmax[k]  = Smin * min( dF[idx]/dXd )
-            Fmin[k]  = Smax * max( dF[idx]/dXd )
-            Finf[k]  = Sinf * max( |Fmin[k]|, |Fmax[k]| )
-
-            where F is an input field,
-                  nb_components = F.nb_components = np.prod(F.shape)
-                  nb_directions = min( F.dim, len(directions))
-                  gradF is an optional output tensor field,
-                  idx is contained in numpy.ndindex(*F.shape)
-                  0 <= j < nb_directions
-                  d = directions[j]
-                  Fmin = created or supplied TensorParameter of shape F.shape + (nb_directions,).
-                  Fmax = created or supplied TensorParameter of shape F.shape + (nb_directions,).
-                  Finf = created or supplied TensorParameter of shape F.shape + (nb_directions,).
-                  Smin = coeffs['Fmin'] or coeffs['Fmin'][k]
-                  Smax = coeffs['Fmax'] or coeffs['Fmax'][k]
-                  Sinf = coeffs['Finf'] or coeffs['Fmax'][k]
-
-        All statistics are only computed if explicitely required by user,
-          unless required to compute another required statistic, see Notes.
-        
-        Parameters
-        ----------
-        F: Field
-            The continuous input field on which the gradient
-            will be taken and statistics will be computed.
-        gradF: Field, optional
-            Optional output field for the gradient.
-            If the gradient is required as an output, one can also use MinMaxStatistics
-            on a precomputed gradient (using the Gradient operator) instead of 
-            MinMaxGradientStatistics.
-        directions: array like of ints, optional
-            The directions in which the statistics are computed,
-            defaults to all directions (ie. range(F.dim)).
-        coeffs: dict of scalar or array like of coefficients, optional
-            Optional scaling of the statistics.
-            Scaling factor should be a scalar or an array-like of scalars for
-            each components of gradF. If not given default to 1 for all statistics.
-        F...: TensorParameter or boolean, optional
-            At least one statistic should be specified (either by boolean or TensorParameter).
-            TensorParameters should be of shape F.shape + (nb_directions,), see Notes.
-            If set to True, the TensorParameter will be generated automatically.
-            Autogenerated TensorParameters that are not required by the user
-            but are generated anyway are set to be quiet.
-            All autogenerated TensorParameters can be retrieved as attributes
-            of this object.
-        all_quiet: bool, optional, defaults to True
-            Set all generated params to be quiet, even the ones that are requested
-            explicitely. 
-        print_tensors: bool, optional, defaults to True
-            Should the phony operator print the tensor parameters during apply ?
-        name: str, optional
-            Name template for generated operator names.
-            Defaults to MinMax({}) where {} will be replaced by gradF[k].name.
-        pretty_name: str, optional
-            Name template for generaed operatr pretty names.
-            Defaults to |+/-{}| where {} will be replaced by gradF[k].pretty_name.
-        pbasename: str, optional
-            Basename for created tensor parameters.
-            Defaults to gradF.name.
-        ppbasename: str, optional
-            Pretty basename for created tensor parameters.
-            Defaults to gradF.pretty_name.
-        variables: dict
-            Dictionary of fields as keys and topologies as values.
-        implementation: hysop.constants.Implementation, optional
-            Specify generated operator underlying backend implementation.
-            Target implementation, should be contained in 
-              MinMaxDerivativeStatistics.available_implementations().
-            If None, implementation will be set to 
-              MinMaxDerivativeStatistics.default_implementation().
-        base_kwds: dict
-            Base class keyword arguments.
-        kwds: dict
-            Additional kwds passed to chosen implementation.
-
-        Attributes:
-        -----------
-        Fmin, Fmax, Finf: TensorParameter
-            All generated tensor parameters.
-            Unused statistics are set to None.
-
-        Notes
-        -----
-        nb_components = F.nb_components
-        nb_directions = min(F.dim, len(directions)).
-        
-        About statistics:
-            Finf requires to compute Fmin and Fmax.
-                Finf = Sinf * max( abs(Smin*Fmin), abs(Smax*Fmax))
-            where Sinf, Smin and Smax are the scaling coefficients defined in coeffs.
-        """
-
-        check_instance(F, Field)
-        check_instance(gradF, Field, allow_none=True)
-        check_instance(directions, tuple, values=int, allow_none=True, 
-                minval=0, maxval=F.dim-1, minsize=1, unique=True)
-        check_instance(coeffs, dict, keys=str, values=(int, float, npw.number), allow_none=True)
-        check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors, allow_none=True)
-        check_instance(name, str, allow_none=True)
-        check_instance(pbasename, str, allow_none=True)
-        check_instance(ppbasename, (str,unicode), allow_none=True)
-        check_instance(implementation, Implementation, allow_none=True)
-        check_instance(base_kwds, dict, allow_none=True)
-        check_instance(all_quiet, bool, allow_none=True)
-
-        if (    ((Fmin is None) or (Fmin is False)) 
-            and ((Fmax is None) or (Fmax is False))
-            and ((Finf is None) or (Finf is False))):
-            msg='No statistics were requested.'
-            msg+='\nPlease specify Fmin, Fmax and/or Finf by either setting '
-            msg+=' their value to True, or by by passing an already existing '
-            msg+=' tensor parameter.'
-            raise ValueError(msg)
-        
-        coeffs     = first_not_None(coeffs, {})
-        variables  = first_not_None(variables, {F: None})
-        all_quiet  = first_not_None(all_quiet, False)
-        
-        directions = to_tuple(first_not_None(directions, range(F.dim)))
-        nb_directions = len(directions)
-        
-        if F.is_tensor:
-            oshape = F.shape + (nb_directions,)
-        else:
-            oshape = (nb_directions,)
-        
-        if (gradF is None):
-            gradF = F.gradient(directions=directions, is_tmp=True)
-        assert (gradF.shape == oshape), gradF.shape
-        
-        variables.setdefault(gradF, variables[F])
-        
-        _names = {
-            'Fmin': '{}_min',
-            'Fmax': '{}_max',
-            'Finf': '{}_inf'
-        }
-
-        _pretty_names = {
-            'Fmin': u'{}\u208b',
-            'Fmax': u'{}\u208a',
-            'Finf': u'|{}|\u208a'
+    @classmethod
+    def implementations(cls):
+        from hysop.backend.host.python.operator.min_max import \
+                PythonMinMaxSpectralDerivativeStatistics
+        from hysop.backend.device.opencl.operator.min_max import \
+                OpenClMinMaxSpectralDerivativeStatistics
+        implementations = {
+                Implementation.PYTHON: PythonMinMaxSpectralDerivativeStatistics,
+                Implementation.OPENCL: OpenClMinMaxSpectralDerivativeStatistics
         }
+        return implementations
 
-        pbasename  = first_not_None(pbasename, gradF.name)
-        ppbasename = first_not_None(ppbasename, gradF.pretty_name)
-        
-        names = { k: v.format(pbasename) for (k,v) in _names.iteritems() }
-        pretty_names = { k: v.format(ppbasename.decode('utf-8')) for (k,v) in _pretty_names.iteritems() }
-
-        def make_param(k, quiet):
-            return TensorParameter(name=names[k], pretty_name=pretty_names[k],
-                    dtype=F.dtype, shape=oshape, quiet=quiet)
-
-        parameters = {}
-        _parameters = dict(Fmin=Fmin, Fmax=Fmax, Finf=Finf)
-        for (k,v) in _parameters.iteritems():
-            param = _parameters[k]
-            if isinstance(param, TensorParameter):
-                pass
-            elif (param is True):
-                param = make_param(k, quiet=all_quiet)
-            elif (_parameters['Finf'] is not None) and ((k == 'Fmin') or (k == 'Fmax')):
-                param = make_param(k, quiet=True)
-            else:
-                param = None
-                setattr(self, k, param)
-                continue
-            assert param.shape == oshape
-            coeffs.setdefault(k, 1)
-            parameters[k] = param
-            setattr(self, k, param)
-
-        unused_coeffs = set(coeffs.keys()) - set(parameters.keys())
-        if unused_coeffs:
-            msg='The following coefficients are not needed: {}'
-            msg=msg.format(unused_coeffs)
-            raise ValueError(unused_coeffs)
-        
-        name = first_not_None(name, 'MinMax({})')
-        pretty_name = first_not_None(pretty_name, u'|\u00b1{}|')
-
-        extra_params = { 'name': gradF.new_empty_array(), 
-                         'pretty_name': gradF.new_empty_array(), 
-                         'coeffs': coeffs, 
-                         'implementation': implementation }
-
-        for (idx, Fi) in gradF.nd_iter():
-            for (statname, stat) in parameters.iteritems():
-                if (stat is None):
-                    continue
-                pname  = _names[statname].format(Fi.name)
-                ppname = _pretty_names[statname].format(Fi.pretty_name.decode('utf-8'))
-                S = stat.view(idx=idx, name=pname, pretty_name=ppname)
-                stats = extra_params.setdefault(statname, gradF.new_empty_array())
-                stats[idx] = S
-            extra_params['name'][idx] = name.format(Fi.name)
-            extra_params['pretty_name'][idx] = pretty_name.format(Fi.pretty_name.decode('utf-8')).encode('utf-8')
-
-        cls = MinMaxDerivativeStatistics
-        super(MinMaxGradientStatistics, self).__init__(F=F, gradF=gradF, 
-                directions=directions, extra_params=extra_params,
-                cls=cls, variables=variables, **kwds)
-
-        # add a phony operator to gather parameter views
-        class MergeTensorViewsOperator(ComputationalGraphOperator):
-            @op_apply
-            def apply(self, **kwds):
-                super(MergeTensorViewsOperator, self).apply(**kwds)
-                if not print_tensors:
-                    return
-                for (k,v) in _parameters.iteritems():
-                    if (v is not None) and (v is not False):
-                        param = parameters[k]
-                        msg='>Parameter {} set to:\n{}'.format(param.pretty_name, param.value)
-                        vprint(msg)
+    @classmethod
+    def default_implementation(cls):
+        return Implementation.PYTHON
 
 
-        _phony_input_params = {}
-        _phony_output_params = { }
-        for pname in _names.keys():
-            if (pname in extra_params):
-                param = parameters[pname]
-                _phony_input_params.update({p.name:p for p in extra_params[pname].ravel()})
-                _phony_output_params[param.name] = param
-        op = MergeTensorViewsOperator(name=name.format(gradF.name), 
-                pretty_name=pretty_name.format(gradF.pretty_name.decode('utf-8')),
-                input_params=_phony_input_params, 
-                output_params=_phony_output_params)
-        self._phony_op = op
+class MinMaxFiniteDifferencesDerivativeStatistics(MinMaxDerivativeStatistics):
+    """
+    Operator frontend to compute min and max statistics on a specific 
+    derivative of a field component using finite differences.
+    """
+    @classmethod
+    def implementations(cls):
+        from hysop.backend.host.python.operator.min_max import \
+                PythonMinMaxFiniteDifferencesDerivativeStatistics
+        from hysop.backend.device.opencl.operator.min_max import \
+                OpenClMinMaxFiniteDifferencesDerivativeStatistics
+        implementations = {
+                Implementation.PYTHON: PythonMinMaxFiniteDifferencesDerivativeStatistics,
+                Implementation.OPENCL: OpenClMinMaxFiniteDifferencesDerivativeStatistics
+        }
+        return implementations
 
-    @debug
-    def _generate(self):
-        """Generate all operators."""
-        operators = super(MinMaxGradientStatistics, self)._generate()
-        operators += (self._phony_op,)
-        return operators
-    
-    @debug
-    def generate_direction(self, i, dt_coeff):
-        # See MultiSpaceDerivatives for the directional interface
-        ops = super(MinMaxGradientStatistics, self).generate_direction(i=i, dt_coeff=dt_coeff)
-        if ops and (i==max(self.directions)):
-            ops += (self._phony_op,)
-        return ops
+    @classmethod
+    def default_implementation(cls):
+        return Implementation.PYTHON
 
diff --git a/hysop/operator/misc.py b/hysop/operator/misc.py
index 38a7aedbbf5084c75d33a0b146f5365643e563cc..13254bb1cb1d451f60540f0e98fd9241403510de 100644
--- a/hysop/operator/misc.py
+++ b/hysop/operator/misc.py
@@ -13,7 +13,8 @@ class Noop(ComputationalGraphOperator):
         """This is a noop."""
         pass
     
-    def supported_backends(self):
+    @classmethod
+    def supported_backends(cls):
         return Backend.all
     
     @classmethod
diff --git a/hysop/operator/penalization.py b/hysop/operator/penalization.py
index d43442294c826930510f4f8b844373079c607ee6..6907e10930870b6e6994fa179a74bccc93fe6a9f 100755
--- a/hysop/operator/penalization.py
+++ b/hysop/operator/penalization.py
@@ -93,7 +93,7 @@ class PenalizeVorticity(ComputationalGraphNodeFrontend):
         check_instance(dt, ScalarParameter)
         check_instance(coeff, (ScalarParameter, float), allow_none=True)
         check_instance(obstacles, (tuple, dict), values=Field,
-                       keys=(ScalarParameter, float))
+                       keys=(ScalarParameter, float), check_kwds=False)
         super(PenalizeVorticity, self).__init__(
             velocity=velocity,
             vorticity=vorticity,
diff --git a/hysop/operator/poisson.py b/hysop/operator/poisson.py
index fe575f60d1e643a49f8f6445ec9a414df9b23115..91208a94ee07199afda36c22ca9a9b356103aef0 100644
--- a/hysop/operator/poisson.py
+++ b/hysop/operator/poisson.py
@@ -8,24 +8,23 @@ from hysop.tools.enum        import EnumFactory
 from hysop.tools.decorators  import debug
 from hysop.fields.continuous_field import Field
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
-from hysop.core.graph.computational_node_frontend import ComputationalGraphNodeFrontend
 
+from hysop.operator.base.spectral_operator import SpectralComputationalGraphNodeFrontend
 from hysop.backend.host.python.operator.poisson import PythonPoisson
 from hysop.backend.device.opencl.operator.poisson import OpenClPoisson
 from hysop.backend.host.fortran.operator.poisson  import PoissonFFTW
 
-class Poisson(ComputationalGraphNodeFrontend):
+class Poisson(SpectralComputationalGraphNodeFrontend):
     """
     Interface the poisson solver.
-    Available implementations are:
-        *PYTHON (numpy local solver)
-        *OPENCL
-        *FORTRAN (FFTW solver)
+    Available implementations are: FORTRAN: FFTW based solver (legacy fortran)
+                                   PYTHON:  generic python fft based solver (pyfftw, scipy or numpy)
+                                   OPENCL:  generic opencl fft based solver (gpyfft)
     """
 
     __implementations = {
-        Implementation.PYTHON: PythonPoisson,
-        Implementation.OPENCL: OpenClPoisson,
+        Implementation.PYTHON:  PythonPoisson,
+        Implementation.OPENCL:  OpenClPoisson,
         Implementation.FORTRAN: PoissonFFTW
     }
 
@@ -74,3 +73,4 @@ class Poisson(ComputationalGraphNodeFrontend):
 
         super(Poisson, self).__init__(Fin=Fin, Fout=Fout,
                 variables=variables, base_kwds=base_kwds, implementation=implementation, **kwds)
+
diff --git a/hysop/operator/poisson_rotational.py b/hysop/operator/poisson_curl.py
similarity index 61%
rename from hysop/operator/poisson_rotational.py
rename to hysop/operator/poisson_curl.py
index 0f78d91daa8d32fccb082c760506e0e6d6181209..1fc61f707f56139b36095f62772627d54c62a0bb 100644
--- a/hysop/operator/poisson_rotational.py
+++ b/hysop/operator/poisson_curl.py
@@ -1,6 +1,6 @@
 """
 @file poisson.py
-PoissonRotational solver frontend.
+PoissonCurl solver frontend.
 """
 from hysop.constants         import Implementation
 from hysop.tools.types       import check_instance
@@ -8,13 +8,13 @@ from hysop.tools.enum        import EnumFactory
 from hysop.tools.decorators  import debug
 from hysop.fields.continuous_field import Field
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
-from hysop.core.graph.computational_node_frontend import ComputationalGraphNodeFrontend
 
-from hysop.backend.host.fortran.operator.poisson_rotational  import FortranPoissonRotational
-from hysop.backend.host.python.operator.poisson_rotational   import PythonPoissonRotational
-from hysop.backend.device.opencl.operator.poisson_rotational import OpenClPoissonRotational
+from hysop.operator.base.spectral_operator import SpectralComputationalGraphNodeFrontend
+from hysop.backend.host.fortran.operator.poisson_curl  import FortranPoissonCurl
+from hysop.backend.host.python.operator.poisson_curl   import PythonPoissonCurl
+from hysop.backend.device.opencl.operator.poisson_curl import OpenClPoissonCurl
 
-class PoissonRotational(ComputationalGraphNodeFrontend):
+class PoissonCurl(SpectralComputationalGraphNodeFrontend):
     """
     Interface the poisson solver.
     Available implementations are:
@@ -24,9 +24,9 @@ class PoissonRotational(ComputationalGraphNodeFrontend):
     """
 
     __implementations = {
-        Implementation.PYTHON: PythonPoissonRotational,
-        Implementation.OPENCL: OpenClPoissonRotational,
-        Implementation.FORTRAN: FortranPoissonRotational
+        Implementation.PYTHON:  PythonPoissonCurl,
+        Implementation.OPENCL:  OpenClPoissonCurl,
+        Implementation.FORTRAN: FortranPoissonCurl
     }
 
     @classmethod
@@ -41,10 +41,18 @@ class PoissonRotational(ComputationalGraphNodeFrontend):
     def __init__(self, velocity, vorticity, variables,
                 implementation=None, base_kwds=None, **kwds):
         """
-        Initialize a PoissonRotational operator frontend for 2D or 3D streamfunction-vorticity formulations.
+        Initialize a PoissonCurl operator frontend for 2D or 3D 
+        streamfunction-vorticity formulations.
 
         in  = W (vorticity)
         out = U (velocity)
+        
+        Vorticity also becomes an output if projection or diffusion is enabled.
+
+        PoissonCurl does more than just solving the Poisson equation for velocity:
+          a/ diffusion of W                        | optional step (enabled with diffusion and dt)
+          b/ projection of W (such that div(U)=0)  | optional step (enabled with projection)
+          c/ poisson solver to recover U from W
 
         About dimensions:
            - if velocity is a 2D vector field, vorticity should have only one component Wx.
@@ -67,6 +75,17 @@ class PoissonRotational(ComputationalGraphNodeFrontend):
             input continuous vorticity field (all components)
         variables: dict
             dictionary of fields as keys and topologies as values.
+        diffusion: ScalarParameter, optional, defaults to None.
+            Diffuse the vorticity field before applying projection and poisson operators.
+            If diffusion is specified, a timestep has to be specified.
+        dt: ScalarParameter, optional, defaults to None
+            Timestep is only required for diffusion.
+            If diffusion is not enabled, this parameter is ignored.
+        projection: hysop.constants.FieldProjection or positive integer, optional
+            Project vorticity such that resolved velocity is divergence free (for 3D fields).
+            When active, projection is done prior to every solve, unless projection is 
+            an integer in which case it is done every given steps.
+            This parameter is ignored for 2D fields and defaults to no projection.
         implementation: Implementation, optional, defaults to None
             target implementation, should be contained in available_implementations().
             If None, implementation will be set to default_implementation().
@@ -79,7 +98,7 @@ class PoissonRotational(ComputationalGraphNodeFrontend):
 
         Notes
         -----
-        A PoissonRotational operator implementation should at least support
+        A PoissonCurl operator implementation should at least support
         the following __init__ attributes: velocity, vorticity, variables
         """
         base_kwds = base_kwds or dict()
@@ -110,5 +129,10 @@ class PoissonRotational(ComputationalGraphNodeFrontend):
             msg='Vorticity component mistmach, got {} components but expected 3.'.format(wcomp)
             raise RuntimeError(msg)
 
-        super(PoissonRotational, self).__init__(velocity=velocity, vorticity=vorticity,
+        if ('nu' in kwds):
+            msg="Diffusion is enabled with the 'diffusion' parameter, not 'nu'."
+            raise ValueError(msg)
+
+        super(PoissonCurl, self).__init__(velocity=velocity, vorticity=vorticity,
                 variables=variables, base_kwds=base_kwds, implementation=implementation, **kwds)
+
diff --git a/hysop/operator/solenoidal_projection.py b/hysop/operator/solenoidal_projection.py
index 00e14e7d7dd05146bd235062b97a470112a03d9f..15ea4c72aeb02b0a8f42527d890261c9aace2d31 100644
--- a/hysop/operator/solenoidal_projection.py
+++ b/hysop/operator/solenoidal_projection.py
@@ -9,12 +9,12 @@ from hysop.tools.enum        import EnumFactory
 from hysop.tools.decorators  import debug
 from hysop.fields.continuous_field import Field
 from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
-from hysop.core.graph.computational_node_frontend import ComputationalGraphNodeFrontend
 
+from hysop.operator.base.spectral_operator import SpectralComputationalGraphNodeFrontend
 from hysop.backend.host.python.operator.solenoidal_projection   import PythonSolenoidalProjection
 from hysop.backend.device.opencl.operator.solenoidal_projection import OpenClSolenoidalProjection
 
-class SolenoidalProjection(ComputationalGraphNodeFrontend):
+class SolenoidalProjection(SpectralComputationalGraphNodeFrontend):
     """
     Interface for solenoidal projection (project a 3d field F such that div(F)=0)
     Available implementations are: 
@@ -32,7 +32,7 @@ class SolenoidalProjection(ComputationalGraphNodeFrontend):
     
     @classmethod
     def default_implementation(cls):
-        return Implementation.OPENCL
+        return Implementation.PYTHON
     
     @debug
     def __init__(self, input_field, output_field, variables, 
diff --git a/hysop/operator/tests/test_analytic.py b/hysop/operator/tests/test_analytic.py
index f5424de33fd33180463e5ebab225894b362949d1..7822a30cab1283a89b42c740950dfd21effe457f 100644
--- a/hysop/operator/tests/test_analytic.py
+++ b/hysop/operator/tests/test_analytic.py
@@ -100,7 +100,7 @@ class TestAnalyticField(object):
     def __analytic_init(cls, data, coords, fns, t):
         assert len(fns) == len(data)
         for (d,fn,coord) in zip(data,fns,coords):
-            d[...] = fn(*(coord+(t(),))).astype(d.dtype)
+            d[...] = npw.asarray(fn(*(coord+(t(),)))).astype(d.dtype)
 
     def _test(self, dim, dtype,
             size_min=None, size_max=None):
@@ -266,13 +266,14 @@ class TestAnalyticField(object):
 
 
     def perform_tests(self):
-        self.test_1d_float32()
-        self.test_2d_float32()
-        self.test_3d_float32()
-
-        self.test_1d_float64()
-        self.test_2d_float64()
-        self.test_3d_float64()
+        if (HYSOP_REAL == npw.float32) or __ENABLE_LONG_TESTS__:
+            self.test_1d_float32()
+            self.test_2d_float32()
+            self.test_3d_float32()
+        if (HYSOP_REAL == npw.float64) or __ENABLE_LONG_TESTS__:
+            self.test_1d_float64()
+            self.test_2d_float64()
+            self.test_3d_float64()
     
 if __name__ == '__main__':
     TestAnalyticField.setup_class(enable_extra_tests=False, 
diff --git a/hysop/operator/tests/test_custom_symbolic.py b/hysop/operator/tests/test_custom_symbolic.py
index f33cd170181930543c45ef62e373e96995965cc4..62dd3e00bc4f95851103d5d99e362545b41a3c12 100644
--- a/hysop/operator/tests/test_custom_symbolic.py
+++ b/hysop/operator/tests/test_custom_symbolic.py
@@ -1,6 +1,7 @@
+
 from hysop import Field, Box
 from hysop.deps import np, it, sm
-from hysop.constants import Implementation, ComputeGranularity, SpaceDiscretization
+from hysop.constants import Implementation, ComputeGranularity, SpaceDiscretization, HYSOP_REAL
 from hysop.testsenv import __ENABLE_LONG_TESTS__, __HAS_OPENCL_BACKEND__
 from hysop.testsenv import opencl_failed, iter_clenv
 from hysop.tools.contexts import printoptions
@@ -22,12 +23,12 @@ from hysop.numerics.odesolvers.runge_kutta import TimeIntegrator, Euler, RK2, RK
 class TestCustomSymbolic(object):
 
     @classmethod
-    def setup_class(cls,
+    def setup_class(cls, 
             enable_extra_tests=__ENABLE_LONG_TESTS__,
             enable_debug_mode=False):
-
+        
         IO.set_default_path('/tmp/hysop_tests/test_custom_symbolic')
-
+        
         if enable_debug_mode:
             cls.size_min0 = 20
             cls.size_max0 = 20
@@ -38,10 +39,10 @@ class TestCustomSymbolic(object):
             cls.size_max0 = 4096
             cls.size_min = 3
             cls.size_max = 16
-
+        
         cls.enable_extra_tests = enable_extra_tests
         cls.enable_debug_mode  = enable_debug_mode
-
+            
         cls.dtypes = [np.int8, np.int16, np.int32, np.int64,
                      np.uint8, np.uint16, np.uint32, np.uint64,
                      np.float32, np.float64]
@@ -55,17 +56,17 @@ class TestCustomSymbolic(object):
         shape = data[0].shape
         if is_integer(dtype):
             for d in data:
-                d[...] = np.random.random_integers(low=0, high=255, size=shape)
+                d[...] = np.random.random_integers(low=0, high=255, size=shape) 
         elif is_fp(dtype):
             for d in data:
                 if pollute:
                     d[...] = np.nan
                 else:
-                    d[...] = np.random.random(size=d.shape)
+                    d[...] = np.random.random(size=d.shape) 
         else:
             msg='Unknown dtype {}.'.format(dtype)
             raise NotImplementedError(msg)
-
+   
     @staticmethod
     def iter_implementations(op_cls, base_kwds):
         for impl in op_cls.implementations():
@@ -73,7 +74,7 @@ class TestCustomSymbolic(object):
             base_kwds['implementation'] = impl
             if impl is Implementation.OPENCL:
                 for cl_env in iter_clenv():
-                    print '      *platform {}, device {}: '.format(cl_env.platform.name.strip(),
+                    print '      *platform {}, device {}: '.format(cl_env.platform.name.strip(), 
                                                           cl_env.device.name.strip()),
                     yield impl, op_cls(cl_env=cl_env, **base_kwds)
             else:
@@ -81,7 +82,7 @@ class TestCustomSymbolic(object):
                 yield impl, op_cls(**base_kwds)
 
     @classmethod
-    def _check_output(cls, impl, op, in_names, refin_buffers, out_names, refout_buffers,
+    def _check_output(cls, impl, op, in_names, refin_buffers, out_names, refout_buffers, 
             out_buffers):
         check_instance(out_buffers, tuple, values=np.ndarray)
         check_instance(refout_buffers, tuple, values=np.ndarray)
@@ -90,7 +91,7 @@ class TestCustomSymbolic(object):
         for i, (oname, out, refout) in enumerate(zip(out_names, out_buffers, refout_buffers)):
             assert refout.dtype == out.dtype, '{} vs {}'.format(refout.dtype, out.dtype)
             assert refout.shape == out.shape, '{} vs {}'.format(refout.shape, out.shape)
-
+            
             has_nan = np.any(np.isnan(refout))
             has_inf = np.any(np.isinf(refout))
             if (out.dtype != refout.dtype):
@@ -102,7 +103,7 @@ class TestCustomSymbolic(object):
 
             has_nan = np.any(np.isnan(out))
             has_inf = np.any(np.isinf(out))
-
+            
             distances = np.abs(out - refout) / np.max(refout)
             if is_integer(out.dtype):
                 mask = (out == refout)
@@ -122,7 +123,7 @@ class TestCustomSymbolic(object):
             if (not has_nan) and (not has_inf) and mask.all():
                 print msg,
                 continue
-            print
+            print 
             print 'Failed to match output of {}:'.format(oname)
             print
             print 'Test output comparisson failed for component {}:'.format(i)
@@ -130,7 +131,7 @@ class TestCustomSymbolic(object):
             print ' *has_inf: {}'.format(has_inf)
             print
             if cls.enable_debug_mode:
-                mask[...] = False
+                mask[...] = False 
             print 'REFERENCE INPUTS:'
             for name, _in in zip(in_names, refin_buffers):
                 print name
@@ -153,16 +154,16 @@ class TestCustomSymbolic(object):
                 print '{} DISTANCES (EPS):'.format(oname)
                 print eps_distances[~mask]
                 print
-
+            
             msg = 'Test failed on component {} for implementation {}.'.format(i, impl)
-            raise RuntimeError(msg)
+            raise RuntimeError(msg) 
         print
-
+    
     def _test_simple(self, dim, _size_min=None, _size_max=None):
         enable_extra_tests = self.enable_extra_tests
         assert dim >= 1
-
-        print 'TEST SIMPLE'
+            
+        print 'TEST SIMPLE' 
         print ' DIM {}'.format(dim)
 
         size_min = first_not_None(_size_min, self.size_min)
@@ -172,31 +173,34 @@ class TestCustomSymbolic(object):
 
         domain = Box(length=(1.0,)*dim)
 
-        discretization  = tuple(np.random.randint(low=size_min, high=size_max+1,
+        discretization  = tuple(np.random.randint(low=size_min, high=size_max+1, 
                                 size=dim-1).tolist())
-        discretization0 = tuple(np.random.randint(low=size_min0, high=size_max0+1,
+        discretization0 = tuple(np.random.randint(low=size_min0, high=size_max0+1, 
                                 size=1).tolist())
         discretization = discretization + discretization0
 
         print ' DISCRETIZATION {}'.format(discretization)
-
-        dtypes = [ (np.float32,), (np.int64,), (np.float64,) ]
-
-        for dtype in dtypes[dim-1]:
+        
+        if __ENABLE_LONG_TESTS__:
+            dtypes = (np.float32, np.float64)
+        else:
+            dtypes = (HYSOP_REAL,)
+    
+        for dtype in dtypes:
             print '  DTYPE {}'.format(dtype.__name__)
-            A = Field(domain=domain, name='A', dtype=dtype,
+            A = Field(domain=domain, name='A', dtype=dtype,  
                     nb_components=1, register_object=False)
-            B = Field(domain=domain, name='B', dtype=dtype,
+            B = Field(domain=domain, name='B', dtype=dtype, 
                     nb_components=1, register_object=False)
-            C = Field(domain=domain, name='C', dtype=dtype,
+            C = Field(domain=domain, name='C', dtype=dtype, 
                     nb_components=3, register_object=False)
-
+            
             P0 = ScalarParameter('P0', dtype=np.float32, initial_value=1.0)
             P1 = ScalarParameter('P1', dtype=np.float64, initial_value=2.0, const=True)
-            T0 = TensorParameter('T0', shape=(13,), dtype=np.int32,
+            T0 = TensorParameter('T0', shape=(13,), dtype=np.int32, 
                     initial_value=np.arange(13, dtype=np.int32))
             T1 = TensorParameter('T1', shape=(3,3), dtype=np.int32)
-
+            
             As = A.s()
             Bs = B.s()
             Cs = C.s()
@@ -205,59 +209,60 @@ class TestCustomSymbolic(object):
 
             for granularity in xrange(dim):
                 print '   GRANULARITY {}'.format(granularity)
-
+                
                 self._test_affect((A,), (42,),      discretization, granularity)
                 self._test_affect((A,B,C), (1,2,3), discretization, granularity)
-
-                expr = Assignment(As, 1+P0s)
-                compute_outputs = lambda ifields, iparams, dfields, ovar, i: \
-                        np.full(fill_value=1+iparams['P0'], shape=ovar.resolution, dtype=ovar.dtype)
-                self._test_expr(expr, compute_outputs,
-                        variables={A:discretization}, method={ComputeGranularity:granularity})
-                P0.value = 4.0
-                compute_outputs = lambda ifields, iparams, dfields, ovar, i: np.full(fill_value=5, shape=ovar.resolution, dtype=ovar.dtype)
-                self._test_expr(expr, compute_outputs,
-                        variables={A:discretization}, method={ComputeGranularity:granularity})
-
-                expr = Assignment(Bs, np.sum(T0s[1::2]))
-                compute_outputs = lambda ifields, iparams, dfields, ovar, i: np.full(fill_value=1+3+5+7+9+11, shape=ovar.resolution, dtype=ovar.dtype)
-                self._test_expr(expr, compute_outputs,
-                        variables={B:discretization}, method={ComputeGranularity:granularity})
-
-                compute_outputs = lambda ifields, iparams, dfields, ovar, i: ifields[C[0]]
-                self._test_expr(Assignment(As, C.s[0]()), compute_outputs,
-                        variables={A:discretization, C:discretization}, method={ComputeGranularity:granularity})
-
+                
+                if __ENABLE_LONG_TESTS__:
+                    expr = Assignment(As, 1+P0s)
+                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: \
+                            np.full(fill_value=1+iparams['P0'], shape=ovar.resolution, dtype=ovar.dtype)
+                    self._test_expr(expr, compute_outputs, 
+                            variables={A:discretization}, method={ComputeGranularity:granularity})
+                    P0.value = 4.0
+                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: np.full(fill_value=5, shape=ovar.resolution, dtype=ovar.dtype)
+                    self._test_expr(expr, compute_outputs, 
+                            variables={A:discretization}, method={ComputeGranularity:granularity})
+                    
+                    expr = Assignment(Bs, np.sum(T0s[1::2]))
+                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: np.full(fill_value=1+3+5+7+9+11, shape=ovar.resolution, dtype=ovar.dtype)
+                    self._test_expr(expr, compute_outputs, 
+                            variables={B:discretization}, method={ComputeGranularity:granularity})
+
+                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: ifields[C[0]]
+                    self._test_expr(Assignment(As, C.s[0]()), compute_outputs, 
+                            variables={A:discretization, C:discretization}, method={ComputeGranularity:granularity}) 
+                
                 compute_outputs = lambda ifields, iparams, dfields, ovar, i: 2*ifields[C[0]] + 8
-                self._test_expr(Assignment(As, 2*C.s[0]()+8), compute_outputs,
-                        variables={A:discretization, C:discretization}, method={ComputeGranularity:granularity})
-
+                self._test_expr(Assignment(As, 2*C.s[0]()+8), compute_outputs, 
+                        variables={A:discretization, C:discretization}, method={ComputeGranularity:granularity}) 
+                
                 if (dim==3):
-                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: ifields[C[0]]*ifields[C[1]]*ifields[C[2]]
-                    self._test_expr(Assignment(As, C.s[0]()*C.s[1]()*C.s[2]()), compute_outputs,
+                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: ifields[C][0]*ifields[C][1]*ifields[C][2]
+                    self._test_expr(Assignment(As, C.s[0]()*C.s[1]()*C.s[2]()), compute_outputs, 
                             variables={A:discretization, C:discretization}, method={ComputeGranularity:granularity})
-
+                    
                     x0  = domain.frame.coords[0]
-                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: 2*ifields[A] - 3*ifields[C[0]]*ifields[C[1]]*ifields[C[2]]
-                    self._test_expr(Assignment(As, 2*As-3*C.s[0]()*C.s[1]()*C.s[2]()), compute_outputs,
+                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: 2*ifields[A] - 3*ifields[C][0]*ifields[C][1]*ifields[C][2]
+                    self._test_expr(Assignment(As, 2*As-3*C.s[0]()*C.s[1]()*C.s[2]()), compute_outputs, 
                             variables={A:discretization, C:discretization}, method={ComputeGranularity:granularity})
-
-                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: (2*np.cos(ifields[C[0]])*np.sin(ifields[C[1]])*np.tan(ifields[C[2]])).astype(ovar.dtype)
-                    self._test_expr(Assignment(As, 2*sm.cos(C.s[0]())*sm.sin(C.s[1]())*sm.tan(C.s[2]())), compute_outputs,
+                    
+                    compute_outputs = lambda ifields, iparams, dfields, ovar, i: (2*np.cos(ifields[C][0])*np.sin(ifields[C][1])*np.tan(ifields[C][2])).astype(ovar.dtype)
+                    self._test_expr(Assignment(As, 2*sm.cos(C.s[0]())*sm.sin(C.s[1]())*sm.tan(C.s[2]())), compute_outputs, 
                             variables={A:discretization, C:discretization}, method={ComputeGranularity:granularity})
-
-    def _test_expr(self, exprs, compute_outputs, variables, method,
+    
+    def _test_expr(self, exprs, compute_outputs, variables, method, 
             apply_kwds=None, no_ref_view=False, dt=None):
         exprs = to_tuple(exprs)
         print '    CustomExpr: {}'.format(' || '.join(str(e) for e in exprs))
-
+        
         assert ComputeGranularity in method
         base_kwds = dict(name='array_affect', exprs=exprs, variables=variables, method=method, dt=dt)
         apply_kwds = first_not_None(apply_kwds, {})
 
         for impl, op in self.iter_implementations(CustomSymbolicOperator, base_kwds):
             problem = op.build()
-
+    
             for field, dfield in problem.iter_output_discrete_fields():
                 dfield.initialize(self.__field_init, dtype=dfield.dtype, pollute=True, only_finite=False)
             for field, dfield in problem.iter_input_discrete_fields():
@@ -290,7 +295,7 @@ class TestCustomSymbolic(object):
                 out_names.append(pname)
 
             problem.apply(**apply_kwds)
-
+            
             out = {'f': {}, 'p': {}, 'dfields': {}}
             for field, ofield in problem.iter_output_discrete_fields(as_scalars=True):
                 out['f'][field] = ()
@@ -307,9 +312,17 @@ class TestCustomSymbolic(object):
                 refin['f'][field] = refin['f'][field][view]
 
             view = dfield.compute_slices
-            flatten = lambda x: tuple(np.asarray(__) for _ in x['f'].values() for __ in _) + \
-                                tuple(np.asarray(_) for _ in x['p'].values())
-
+            def flatten(x):
+                out = ()
+                for _ in x['f'].values():
+                    if not isinstance(_, tuple):
+                        _ = (_,)
+                    for __ in _:
+                        out += (__,)
+                for _ in x['p'].values():
+                    out += (np.asarray(_),)
+                return out
+            
             self._check_output(impl, op, in_names, flatten(refin), out_names, flatten(refout), flatten(out))
 
     def _test_affect(self, fields, rhs, discretization, granularity):
@@ -320,9 +333,9 @@ class TestCustomSymbolic(object):
             for i in xrange(f.nb_components):
                 e = Assignment(f.s[i](),c)
                 exprs += (e,)
-
+        
         print '    CustomExpr: {}'.format(' || '.join(str(e) for e in exprs))
-
+        
         method={ComputeGranularity: granularity}
         base_kwds = dict(name='custom_affect', exprs=exprs, variables=variables, method=method)
         for impl, op in self.iter_implementations(CustomSymbolicOperator, base_kwds):
@@ -337,9 +350,9 @@ class TestCustomSymbolic(object):
                 for i in xrange(dfield.nb_components):
                     refin += (dfield.data[i].get().handle[view],)
                     in_names.append(ofield.name+'::{}'.format(i))
-
+            
             problem.apply()
-
+            
             out = ()
             refout = ()
             out_names = []
@@ -361,7 +374,7 @@ class TestCustomSymbolic(object):
     def _test_stencil(self, dim, _size_min=None, _size_max=None):
         enable_extra_tests = self.enable_extra_tests
         assert dim >= 1
-        print 'TEST STENCIL'
+        print 'TEST STENCIL' 
         print ' DIM {}'.format(dim)
 
         size_min = first_not_None(_size_min, self.size_min)
@@ -372,23 +385,23 @@ class TestCustomSymbolic(object):
         domain = Box(length=(1.0,)*dim)
         frame = domain.frame
 
-        discretization  = tuple(np.random.randint(low=size_min, high=size_max+1,
+        discretization  = tuple(np.random.randint(low=size_min, high=size_max+1, 
                                 size=dim-1).tolist())
-        discretization0 = tuple(np.random.randint(low=size_min0, high=size_max0+1,
+        discretization0 = tuple(np.random.randint(low=size_min0, high=size_max0+1, 
                                 size=1).tolist())
         discretization = discretization + discretization0
 
         print ' DISCRETIZATION {}'.format(discretization)
-        A = Field(domain=domain, name='A', dtype=np.float32,
+        A = Field(domain=domain, name='A', dtype=np.float32,  
                 nb_components=1, register_object=False)
-        B = Field(domain=domain, name='B', dtype=np.float32,
+        B = Field(domain=domain, name='B', dtype=np.float32, 
                 nb_components=2, register_object=False)
-        C = Field(domain=domain, name='C', dtype=np.float64,
+        C = Field(domain=domain, name='C', dtype=np.float64, 
                 nb_components=3, register_object=False)
 
         P0 = ScalarParameter('P0', dtype=np.float32, initial_value=3.14)
         T0 = TensorParameter('T',  shape=(3,), dtype=np.int32, initial_value=[4,3,2])
-
+        
         As = A.s()
         Bs = B.s()
         Cs = C.s()
@@ -396,49 +409,55 @@ class TestCustomSymbolic(object):
         T0s = T0.s
 
         x0  = frame.coords[0]
-
+        
         csg = CenteredStencilGenerator()
         csg.configure(dtype=MPQ, dim=1)
 
-        for order in [2, 4]:
+        if __ENABLE_LONG_TESTS__:
+            orders = (2,4)
+        else:
+            orders = (4,)
+        
+        for order in orders:
             print '  ORDER {}'.format(order)
             for granularity in xrange(dim):
                 print '   GRANULARITY {}'.format(granularity)
+                
+                if __ENABLE_LONG_TESTS__:
+                    expr    = Assignment(As, B.s[0]().diff(x0,x0))
+                    stencil = csg.generate_exact_stencil(derivative=2, order=order)
+                    def compute_outputs(fields, iparams, ifields, ovar, i):
+                        return stencil.apply(fields[B[0]], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)[ifields[B[0]].compute_slices]
+                    self._test_expr(expr, compute_outputs, 
+                            variables={A:discretization, B:discretization}, 
+                            method={ComputeGranularity:granularity, SpaceDiscretization:order})
+                    
+                    
+                    expr    = Assignment(As, 1+2*B.s[0]().diff(x0))
+                    stencil = csg.generate_exact_stencil(derivative=1, order=order)
+                    def compute_outputs(fields, iparams, ifields, ovar, i):
+                        res = stencil.apply(fields[B[0]], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)[ifields[B[0]].compute_slices]
+                        return 1+2*res
+                    self._test_expr(expr, compute_outputs, 
+                            variables={A:discretization, B:discretization}, 
+                            method={ComputeGranularity:granularity, SpaceDiscretization:order})
+                    
+                    expr    = Assignment(Bs[0](), (1+np.dot([3,-2], Bs.diff(x0)))*As.diff(x0))
+
+                    stencil = csg.generate_exact_stencil(derivative=1, order=order)
+                    def compute_outputs(fields, iparams, ifields, ovar, i):
+                        if (i==0):
+                            res0 = stencil.apply(fields[B[0]], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)
+                            res1 = stencil.apply(fields[B[1]], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)
+                            res2 = stencil.apply(fields[A], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)
+                            return (1+(3*res0)-(2*res1))*res2
+                        else:
+                            return ovar.data[i].get().handle
 
-                expr    = Assignment(As, B.s[0]().diff(x0,x0))
-                stencil = csg.generate_exact_stencil(derivative=2, order=order)
-                def compute_outputs(fields, iparams, ifields, ovar, i):
-                    return stencil.apply(fields[B[0]], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)[ifields[B[0]].compute_slices]
-                self._test_expr(expr, compute_outputs,
-                        variables={A:discretization, B:discretization},
-                        method={ComputeGranularity:granularity, SpaceDiscretization:order})
-
-
-                expr    = Assignment(As, 1+2*B.s[0]().diff(x0))
-                stencil = csg.generate_exact_stencil(derivative=1, order=order)
-                def compute_outputs(fields, iparams, ifields, ovar, i):
-                    res = stencil.apply(fields[B[0]], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)[ifields[B[0]].compute_slices]
-                    return 1+2*res
-                self._test_expr(expr, compute_outputs,
-                        variables={A:discretization, B:discretization},
-                        method={ComputeGranularity:granularity, SpaceDiscretization:order})
-
-                expr    = Assignment(Bs[0](), (1+np.dot([3,-2], Bs.diff(x0)))*As.diff(x0))
-
-                stencil = csg.generate_exact_stencil(derivative=1, order=order)
-                def compute_outputs(fields, iparams, ifields, ovar, i):
-                    if (i==0):
-                        res0 = stencil.apply(fields[B[0]], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)
-                        res1 = stencil.apply(fields[B[1]], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)
-                        res2 = stencil.apply(fields[A], symbols={stencil.dx:ovar.space_step[-1]}, axis=-1)
-                        return (1+(3*res0)-(2*res1))*res2
-                    else:
-                        return ovar.data[i].get().handle
-
-                self._test_expr(expr, compute_outputs,
-                        variables={A:discretization, B:discretization},
-                        method={ComputeGranularity:granularity, SpaceDiscretization:order})
-
+                    self._test_expr(expr, compute_outputs, 
+                            variables={A:discretization, B:discretization}, 
+                            method={ComputeGranularity:granularity, SpaceDiscretization:order})
+                
                 expr0    = Assignment(B.s[0](), (1+np.dot([3,-2], Bs.diff(x0)))*As.diff(x0))
                 expr1    = Assignment(B.s[1](), (1-np.dot([3,-2], Bs.diff(x0)))*As.diff(x0))
                 expr2    = Assignment(A.s[0](), 0.27 + 1.57*sm.cos(Bs[0].diff(x0))*sm.sin(Bs[1].diff(x0)))
@@ -454,10 +473,10 @@ class TestCustomSymbolic(object):
                             return (1-(3*res0)+(2*res1))*res2
                     else:
                         return 0.27 + 1.57*np.cos(res0)*np.sin(res1)
-                self._test_expr(expr, compute_outputs,
-                        variables={A:discretization, B:discretization},
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization, B:discretization}, 
                         method={ComputeGranularity:granularity, SpaceDiscretization:order})
-
+                
                 expr = Assignment(B.s[0](), T0s[2]*((4+P0s)*As*B.s[0]().diff(x0)+T0s[1]*Bs[1]).diff(x0) + P0s*T0s[0])
                 stencil0 = csg.generate_exact_stencil(derivative=1, order=order)
                 stencil1 = csg.generate_exact_stencil(derivative=2, order=order)
@@ -475,15 +494,15 @@ class TestCustomSymbolic(object):
                         return P0[0]*T0[0] + T0[2]*((4+P0[0])*(A0*d2B0+dA0*dB0) + T0[1]*dB1)
                     else:
                         return ovar.dfield.data[i].get().handle
-                self._test_expr(expr, compute_outputs,
-                        variables={A:discretization, B:discretization},
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization, B:discretization}, 
                         method={ComputeGranularity:granularity, SpaceDiscretization:order})
-
-
+    
+                
     def _test_time_integrator(self, dim, _size_min=None, _size_max=None):
         enable_extra_tests = self.enable_extra_tests
         assert dim >= 1
-        print 'TEST INTEGRATOR'
+        print 'TEST INTEGRATOR' 
         print ' DIM {}'.format(dim)
 
         size_min = first_not_None(_size_min, self.size_min)
@@ -494,38 +513,38 @@ class TestCustomSymbolic(object):
         domain = Box(length=(1.0,)*dim)
         frame = domain.frame
 
-        discretization  = tuple(np.random.randint(low=size_min, high=size_max+1,
+        discretization  = tuple(np.random.randint(low=size_min, high=size_max+1, 
                                 size=dim-1).tolist())
-        discretization0 = tuple(np.random.randint(low=size_min0, high=size_max0+1,
+        discretization0 = tuple(np.random.randint(low=size_min0, high=size_max0+1, 
                                 size=1).tolist())
         discretization = discretization + discretization0
 
         print ' DISCRETIZATION {}'.format(discretization)
-        A = Field(domain=domain, name='A', dtype=np.float32,
+        A = Field(domain=domain, name='A', dtype=np.float32,  
                 nb_components=1, register_object=False)
-        B = Field(domain=domain, name='B', dtype=np.float32,
+        B = Field(domain=domain, name='B', dtype=np.float32, 
                 nb_components=2, register_object=False)
-        C = Field(domain=domain, name='C', dtype=np.float32,
+        C = Field(domain=domain, name='C', dtype=np.float32, 
                 nb_components=3, register_object=False)
-
+            
         P0 = ScalarParameter('P0', dtype=np.float32, initial_value=3.14)
         T0 = TensorParameter('T',  shape=(3,), dtype=np.int32, initial_value=[4,3,2])
-
+        
         As = A.s()
         Bs = B.s()
         Cs = C.s()
         P0s = P0.s
         T0s = T0.s
-
+        
         T  = ScalarParameter(name=frame.time,   dtype=np.float32, initial_value=0.0)
         DT = ScalarParameter(name=dtime_symbol, dtype=np.float32, initial_value=0.1)
 
         t = T.s
         x0  = frame.coords[0]
-
+        
         csg = CenteredStencilGenerator()
         csg.configure(dtype=MPQ, dim=1)
-
+        
         order = 4
         granularity = 0
         print '  GRANULARITY {}'.format(granularity)
@@ -534,167 +553,173 @@ class TestCustomSymbolic(object):
         D1 = csg.generate_exact_stencil(derivative=1, order=order)
         D2 = csg.generate_exact_stencil(derivative=2, order=order)
         D3 = csg.generate_exact_stencil(derivative=3, order=order)
+        
+        if __ENABLE_LONG_TESTS__:
+            integrators = [Euler, RK2, RK4, RK4_38]
+        else:
+            integrators = (RK2,)
 
-        for integrator in [Euler, RK2, RK4, RK4_38]:
+        for integrator in integrators:
             print '   INTEGRATOR {}'.format(integrator)
+            
+            if __ENABLE_LONG_TESTS__:
+                expr = Assignment(As.diff(t), 0)
+                def compute_outputs(fields, iparams, ifields, ovar, i):
+                    A0 = fields[A]
+                    if (ovar.dfield._field is A) and (i==0):
+                        return fields[A].copy()
+                    else:
+                        return ovar.dfield.data[i].get().handle
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization, B:discretization, C:discretization}, 
+                        method={ComputeGranularity:granularity, SpaceDiscretization:order, 
+                                TimeIntegrator:integrator}, dt=DT)
 
-            expr = Assignment(As.diff(t), 0)
-            def compute_outputs(fields, iparams, ifields, ovar, i):
-                A0 = fields[A]
-                if (ovar.dfield._field is A) and (i==0):
-                    return fields[A].copy()
-                else:
-                    return ovar.dfield.data[i].get().handle
-            self._test_expr(expr, compute_outputs,
-                    variables={A:discretization, B:discretization, C:discretization},
-                    method={ComputeGranularity:granularity, SpaceDiscretization:order,
-                            TimeIntegrator:integrator}, dt=DT)
-
-            expr = Assignment(As.diff(t), 1)
-            def compute_outputs(fields, iparams, ifields, ovar, i):
-                A0 = fields[A]
-                if (ovar.dfield._field is A) and (i==0):
-                    Xin  = { 'a':  fields[A] }
-                    Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
-                    def rhs(out, X, t, **kwds):
-                        out['a'][...] = 1
-                    integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
-                    return Xout['a']
-                else:
-                    return ovar.dfield.data[i].get().handle
-            self._test_expr(expr, compute_outputs,
-                    variables={A:discretization, B:discretization, C:discretization},
-                    method={ComputeGranularity:granularity, SpaceDiscretization:order,
-                            TimeIntegrator:integrator}, dt=DT)
-
-
-            expr = Assignment(As.diff(t), np.dot([2,-3], Bs))
-            def compute_outputs(fields, iparams, ifields, ovar, i):
-                A0 = fields[A]
-                B0 = fields[B[0]]
-                B1 = fields[B[1]]
-                if (ovar.dfield._field is A) and (i==0):
-                    Xin  = { 'a': A0 }
-                    Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
-                    def rhs(out, X, t, **kwds):
-                        out['a'][...] = 2*B0 -3*B1
-                    integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
-                    return Xout['a']
-                else:
-                    return ovar.dfield.data[i].get().handle
-            self._test_expr(expr, compute_outputs,
-                    variables={A:discretization, B:discretization, C:discretization},
-                    method={ComputeGranularity:granularity, SpaceDiscretization:order,
-                            TimeIntegrator:integrator}, dt=DT)
-
-            expr = Assignment(As.diff(t), As*Bs[0] + Bs[1])
-            def compute_outputs(fields, iparams, ifields, ovar, i):
-                A0 = fields[A]
-                B0 = fields[B[0]]
-                B1 = fields[B[1]]
-                if (ovar.dfield._field is A) and (i==0):
-                    Xin  = { 'a': A0 }
-                    Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
-                    def rhs(out, X, t, **kwds):
-                        out['a'][...] = X['a']*B0 + B1
-                    integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
-                    return Xout['a']
-                else:
-                    return ovar.dfield.data[i].get().handle
-            self._test_expr(expr, compute_outputs,
-                    variables={A:discretization, B:discretization, C:discretization},
-                    method={ComputeGranularity:granularity, SpaceDiscretization:order,
-                            TimeIntegrator:integrator}, dt=DT)
-
-            expr0 = Assignment(Bs[0].diff(t), 1+Bs[1])
-            expr1 = Assignment(Bs[1].diff(t), 1-Bs[0])
-            expr = (expr0, expr1)
-            def compute_outputs(fields, iparams, ifields, ovar, i):
-                B0 = fields[B[0]]
-                B1 = fields[B[1]]
-                if (ovar.dfield._field in B.fields):
-                    Xin  = { 'b0': B0, 'b1': B1 }
-                    Xout = { 'b0': np.empty_like(B0[ifields[B[0]].compute_slices]),
-                             'b1': np.empty_like(B1[ifields[B[1]].compute_slices]) }
-                    def rhs(out, X, t, **kwds):
-                        out['b0'][...] = 1+X['b1']
-                        out['b1'][...] = 1-X['b0']
-                    integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
-                    varname = 'b0' if (ovar.dfield._field is B[0]) else 'b1'
-                    return Xout[varname]
-                else:
-                    return ovar.dfield.data[i].get().handle
-            self._test_expr(expr, compute_outputs,
-                    variables={A:discretization, B:discretization, C:discretization},
-                    method={ComputeGranularity:granularity, SpaceDiscretization:order,
-                            TimeIntegrator:integrator}, dt=DT)
-
-
-            expr = Assignment(As.diff(t), Bs[0].diff(x0))
-            def compute_outputs(fields, iparams, ifields, ovar, i):
-                A0 = fields[A]
-                B0 = fields[B[0]]
-                dB0 = D1.apply(B0, symbols={D1.dx:ovar.space_step[-1]}, axis=-1)
-                if (ovar.dfield._field is A):
-                    Xin  = { 'a': A0 }
-                    Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
-                    def rhs(out, X, t, **kwds):
-                        out['a'][...] = dB0[ifields[B[0]].compute_slices]
-                    integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
-                    return Xout['a']
-                else:
-                    return ovar.dfield.data[i].get().handle
-            self._test_expr(expr, compute_outputs,
-                    variables={A:discretization, B:discretization},
-                    method={ComputeGranularity:granularity, SpaceDiscretization:order,
-                            TimeIntegrator:integrator}, dt=DT)
-
-
-            expr = Assignment(As.diff(t), As.diff(x0))
-            def compute_outputs(fields, iparams, ifields, ovar, i):
-                A0 = fields[A]
-                if (ovar.dfield._field is A):
-                    Xin  = { 'a': A0 }
-                    Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
-                    views = { 'a': ifields[A].compute_slices }
-                    def rhs(out, X, t, **kwds):
-                        dA0 = D1.apply(X['a'], symbols={D1.dx:ovar.space_step[-1]}, axis=-1)
-                        out['a'][...] = dA0
-                    integrator(Xin=Xin, Xout=Xout, views=views, RHS=rhs, t=0.0, dt=DT())
-                    return Xout['a']
-                else:
-                    return ovar.dfield.data[i].get().handle
-            self._test_expr(expr, compute_outputs,
-                    variables={A:discretization},
-                    method={ComputeGranularity:granularity, SpaceDiscretization:order,
-                            TimeIntegrator:integrator}, dt=DT, no_ref_view=True)
-
-            expr = Assignment(As.diff(t), P0s*Bs[0]*As.diff(x0) + Bs[1].diff(x0,x0)*As)
-            def compute_outputs(fields, iparams, ifields, ovar, i):
-                A0 = fields[A]
-                B0 = fields[B[0]]
-                B1 = fields[B[1]]
-                d2B1 = D2.apply(B1, symbols={D2.dx:ovar.space_step[-1]}, axis=-1)
-                assert order%2==0
-                bg = order//2
-                v = (Ellipsis, slice(bg, -bg))
-                if (ovar.dfield._field is A):
-                    Xin  = { 'a': A0 }
-                    Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
-                    views = { 'a': ifields[A].compute_slices }
-                    def rhs(out, X, t, **kwds):
-                        A0 = X['a']
-                        dA0 = D1.apply(A0, symbols={D1.dx:ovar.space_step[-1]}, axis=-1)
-                        out['a'][v] = P0()*B0*dA0[v] + d2B1[v]*A0[v]
-                    integrator(Xin=Xin, Xout=Xout, views=views, RHS=rhs, t=0.0, dt=DT())
-                    return Xout['a']
-                else:
-                    return ovar.dfield.data[i].get().handle
-            self._test_expr(expr, compute_outputs,
-                    variables={A:discretization, B:discretization},
-                    method={ComputeGranularity:granularity, SpaceDiscretization:order,
-                            TimeIntegrator:integrator}, dt=DT, no_ref_view=True)
-
+                expr = Assignment(As.diff(t), 1)
+                def compute_outputs(fields, iparams, ifields, ovar, i):
+                    A0 = fields[A]
+                    if (ovar.dfield._field is A) and (i==0):
+                        Xin  = { 'a':  fields[A] }
+                        Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
+                        def rhs(out, X, t, **kwds):
+                            out['a'][...] = 1
+                        integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
+                        return Xout['a']
+                    else:
+                        return ovar.dfield.data[i].get().handle
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization, B:discretization, C:discretization}, 
+                        method={ComputeGranularity:granularity, SpaceDiscretization:order, 
+                                TimeIntegrator:integrator}, dt=DT)
+                
+                
+                expr = Assignment(As.diff(t), np.dot([2,-3], Bs))
+                def compute_outputs(fields, iparams, ifields, ovar, i):
+                    A0 = fields[A]
+                    B0 = fields[B[0]]
+                    B1 = fields[B[1]]
+                    if (ovar.dfield._field is A) and (i==0):
+                        Xin  = { 'a': A0 }
+                        Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
+                        def rhs(out, X, t, **kwds):
+                            out['a'][...] = 2*B0 -3*B1
+                        integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
+                        return Xout['a']
+                    else:
+                        return ovar.dfield.data[i].get().handle
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization, B:discretization, C:discretization}, 
+                        method={ComputeGranularity:granularity, SpaceDiscretization:order, 
+                                TimeIntegrator:integrator}, dt=DT)
+                
+                expr = Assignment(As.diff(t), As*Bs[0] + Bs[1])
+                def compute_outputs(fields, iparams, ifields, ovar, i):
+                    A0 = fields[A]
+                    B0 = fields[B[0]]
+                    B1 = fields[B[1]]
+                    if (ovar.dfield._field is A) and (i==0):
+                        Xin  = { 'a': A0 }
+                        Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
+                        def rhs(out, X, t, **kwds):
+                            out['a'][...] = X['a']*B0 + B1
+                        integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
+                        return Xout['a']
+                    else:
+                        return ovar.dfield.data[i].get().handle
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization, B:discretization, C:discretization}, 
+                        method={ComputeGranularity:granularity, SpaceDiscretization:order, 
+                                TimeIntegrator:integrator}, dt=DT)
+                
+                expr0 = Assignment(Bs[0].diff(t), 1+Bs[1])
+                expr1 = Assignment(Bs[1].diff(t), 1-Bs[0])
+                expr = (expr0, expr1)
+                def compute_outputs(fields, iparams, ifields, ovar, i):
+                    B0 = fields[B[0]]
+                    B1 = fields[B[1]]
+                    if (ovar.dfield._field in B.fields):
+                        Xin  = { 'b0': B0, 'b1': B1 }
+                        Xout = { 'b0': np.empty_like(B0[ifields[B[0]].compute_slices]),
+                                 'b1': np.empty_like(B1[ifields[B[1]].compute_slices]) }
+                        def rhs(out, X, t, **kwds):
+                            out['b0'][...] = 1+X['b1']
+                            out['b1'][...] = 1-X['b0']
+                        integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
+                        varname = 'b0' if (ovar.dfield._field is B[0]) else 'b1'
+                        return Xout[varname]
+                    else:
+                        return ovar.dfield.data[i].get().handle
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization, B:discretization, C:discretization}, 
+                        method={ComputeGranularity:granularity, SpaceDiscretization:order, 
+                                TimeIntegrator:integrator}, dt=DT)
+                
+                
+                expr = Assignment(As.diff(t), Bs[0].diff(x0))
+                def compute_outputs(fields, iparams, ifields, ovar, i):
+                    A0 = fields[A]
+                    B0 = fields[B[0]]
+                    dB0 = D1.apply(B0, symbols={D1.dx:ovar.space_step[-1]}, axis=-1)
+                    if (ovar.dfield._field is A):
+                        Xin  = { 'a': A0 }
+                        Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
+                        def rhs(out, X, t, **kwds):
+                            out['a'][...] = dB0[ifields[B[0]].compute_slices]
+                        integrator(Xin=Xin, Xout=Xout, RHS=rhs, t=0.0, dt=DT())
+                        return Xout['a']
+                    else:
+                        return ovar.dfield.data[i].get().handle
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization, B:discretization}, 
+                        method={ComputeGranularity:granularity, SpaceDiscretization:order, 
+                                TimeIntegrator:integrator}, dt=DT)
+                
+                
+                expr = Assignment(As.diff(t), As.diff(x0))
+                def compute_outputs(fields, iparams, ifields, ovar, i):
+                    A0 = fields[A]
+                    if (ovar.dfield._field is A):
+                        Xin  = { 'a': A0 }
+                        Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
+                        views = { 'a': ifields[A].compute_slices }
+                        def rhs(out, X, t, **kwds):
+                            dA0 = D1.apply(X['a'], symbols={D1.dx:ovar.space_step[-1]}, axis=-1)
+                            out['a'][...] = dA0
+                        integrator(Xin=Xin, Xout=Xout, views=views, RHS=rhs, t=0.0, dt=DT())
+                        return Xout['a']
+                    else:
+                        return ovar.dfield.data[i].get().handle
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization}, 
+                        method={ComputeGranularity:granularity, SpaceDiscretization:order, 
+                                TimeIntegrator:integrator}, dt=DT, no_ref_view=True)
+                
+                expr = Assignment(As.diff(t), P0s*Bs[0]*As.diff(x0) + Bs[1].diff(x0,x0)*As)
+                def compute_outputs(fields, iparams, ifields, ovar, i):
+                    A0 = fields[A]
+                    B0 = fields[B[0]]
+                    B1 = fields[B[1]]
+                    d2B1 = D2.apply(B1, symbols={D2.dx:ovar.space_step[-1]}, axis=-1)
+                    assert order%2==0
+                    bg = order//2
+                    v = (Ellipsis, slice(bg, -bg))
+                    if (ovar.dfield._field is A):
+                        Xin  = { 'a': A0 }
+                        Xout = { 'a': np.empty_like(Xin['a'][ifields[A].compute_slices]) }
+                        views = { 'a': ifields[A].compute_slices }
+                        def rhs(out, X, t, **kwds):
+                            A0 = X['a']
+                            dA0 = D1.apply(A0, symbols={D1.dx:ovar.space_step[-1]}, axis=-1)
+                            out['a'][v] = P0()*B0*dA0[v] + d2B1[v]*A0[v]
+                        integrator(Xin=Xin, Xout=Xout, views=views, RHS=rhs, t=0.0, dt=DT())
+                        return Xout['a']
+                    else:
+                        return ovar.dfield.data[i].get().handle
+                self._test_expr(expr, compute_outputs, 
+                        variables={A:discretization, B:discretization}, 
+                        method={ComputeGranularity:granularity, SpaceDiscretization:order, 
+                                TimeIntegrator:integrator}, dt=DT, no_ref_view=True)
+            
             expr0 = Assignment(Bs[0].diff(t), 1 - As*Bs[1].diff(x0))
             expr1 = Assignment(Bs[1].diff(t), 1 + As*Bs[0].diff(x0))
             expr = (expr0, expr1)
@@ -702,13 +727,13 @@ class TestCustomSymbolic(object):
                 A0 = fields[A]
                 B0 = fields[B[0]]
                 B1 = fields[B[1]]
-                b0_view = ifields[B[0]].compute_slices
-                b1_view = ifields[B[1]].compute_slices
+                b0_view = ifields[B[0]].compute_slices 
+                b1_view = ifields[B[1]].compute_slices 
                 assert order%2==0
                 bg = order//2
                 v = (Ellipsis, slice(bg, -bg))
                 if (ovar.dfield._field in B.fields):
-                    Xin   = { 'b0': B0,
+                    Xin   = { 'b0': B0, 
                               'b1': B1 }
                     Xout  = { 'b0': np.empty_like(Xin['b0'][ifields[B[0]].compute_slices]),
                               'b1': np.empty_like(Xin['b1'][ifields[B[1]].compute_slices]) }
@@ -728,9 +753,9 @@ class TestCustomSymbolic(object):
                     return Xout[varname]
                 else:
                     return ovar.dfield.data[i].get().handle
-            self._test_expr(expr, compute_outputs,
-                    variables={A:discretization, B:discretization},
-                    method={ComputeGranularity:granularity, SpaceDiscretization:order,
+            self._test_expr(expr, compute_outputs, 
+                    variables={A:discretization, B:discretization}, 
+                    method={ComputeGranularity:granularity, SpaceDiscretization:order, 
                             TimeIntegrator:integrator}, dt=DT, no_ref_view=True)
 
     def test_simple_1d(self):
@@ -755,22 +780,25 @@ class TestCustomSymbolic(object):
     def perform_tests(self):
         self.test_simple_1d()
         self.test_simple_2d()
-        self.test_simple_3d()
-
+        if __ENABLE_LONG_TESTS__:
+            self.test_simple_3d()
+       
         self.test_stencil_1d()
         self.test_stencil_2d()
-        self.test_stencil_3d()
+        if __ENABLE_LONG_TESTS__:
+            self.test_stencil_3d()
 
         self.test_time_integrator_1d()
         self.test_time_integrator_2d()
-        self.test_time_integrator_3d()
-
+        if __ENABLE_LONG_TESTS__:
+            self.test_time_integrator_3d()
+    
 if __name__ == '__main__':
-    TestCustomSymbolic.setup_class(enable_extra_tests=False,
+    TestCustomSymbolic.setup_class(enable_extra_tests=False, 
                                       enable_debug_mode=False)
-
+    
     enable_pretty_printing()
-
+    
     test = TestCustomSymbolic()
     test.perform_tests()
 
diff --git a/hysop/operator/tests/test_diffusion.py b/hysop/operator/tests/test_diffusion.py
index 4e34bac1859decd1e707d6e7f247f01ae795d90b..ddf2cbf9a4eaacb57ef553548c9a1d703454b449 100644
--- a/hysop/operator/tests/test_diffusion.py
+++ b/hysop/operator/tests/test_diffusion.py
@@ -1,6 +1,6 @@
 import numpy as np
 import sys, random
-from hysop.constants import HYSOP_REAL
+from hysop.constants import HYSOP_REAL, BoundaryCondition
 from hysop.tools.numpywrappers import npw
 from hysop.tools.contexts import printoptions
 from hysop.tools.types import first_not_None, to_tuple
@@ -23,12 +23,8 @@ class TestDiffusionOperator(object):
 
         IO.set_default_path('/tmp/hysop_tests/test_diffusion')
 
-        if enable_debug_mode:
-            cls.size_min = 8
-            cls.size_max = 9
-        else:
-            cls.size_min = 32
-            cls.size_max = 64
+        cls.size_min = 8
+        cls.size_max = 16
 
         cls.enable_extra_tests = enable_extra_tests
         cls.enable_debug_mode  = enable_debug_mode
@@ -47,11 +43,15 @@ class TestDiffusionOperator(object):
         size_max = first_not_None(size_max, self.size_max) + 1
 
         shape = tuple(npw.random.randint(low=size_min, high=size_max, size=dim).tolist())
-
-        flt_types = (npw.float32, npw.float64)
+    
+        if __ENABLE_LONG_TESTS__:
+            flt_types = (npw.float32, npw.float64)
+        else:
+            flt_types = (HYSOP_REAL,)
 
         domain = Box(length=(2*npw.pi,)*dim)
         for dtype in flt_types:
+            nu = ScalarParameter('nu', dtype=dtype, initial_value=random.random(), const=True)
             nb_components = 5 if (dim==2) else 6
             Fin  = Field(domain=domain, name='Fin', dtype=dtype,
                     nb_components=nb_components, register_object=False)
@@ -59,7 +59,7 @@ class TestDiffusionOperator(object):
                    nb_components=nb_components, register_object=False)
             self._test_one(shape=shape, dim=dim, dtype=dtype,
                     is_inplace=is_inplace, domain=domain,
-                    Fin=Fin, Fout=Fout, viscosity=random.random())
+                    Fin=Fin, Fout=Fout, nu=nu)
 
     @staticmethod
     def __random_init(data, coords, dtype):
@@ -85,7 +85,7 @@ class TestDiffusionOperator(object):
             raise NotImplementedError(msg)
 
     def _test_one(self, shape, dim,
-            dtype, is_inplace, domain, Fin, Fout, viscosity):
+            dtype, is_inplace, domain, Fin, Fout, nu):
         print
         print '\nTesting {}D Diffusion: inplace={} dtype={} shape={}'.format(
             dim, is_inplace, dtype.__name__, shape),
@@ -105,23 +105,43 @@ class TestDiffusionOperator(object):
 
         implementations = Diffusion.implementations()
         ref_impl = Implementation.PYTHON # ref impl is always the first
-
+        
         def iter_impl(impl):
-            base_kwds = dict(Fin=fin, viscosity=viscosity, dt=dt,
-                    variables=variables, implementation=impl,
+            base_kwds = dict(Fin=fin, nu=nu, dt=dt,
+                    variables=variables, implementation=impl, 
                     name='test_diffusion_{}'.format(str(impl).lower()))
             if not is_inplace:
                 base_kwds['Fout'] = fout
             if (impl is Implementation.PYTHON):
+                msg='   *Python FFTW: '
+                print msg,
                 diff = Diffusion(**base_kwds)
-                msg=''
-                yield msg, diff.to_graph()
+                yield diff.to_graph()
             elif (impl is Implementation.FORTRAN):
-                if (dtype is not HYSOP_REAL):
-                    print 'NO SUPPORT for ' + str(dtype),
-                    return
+                msg='   *Fortran FFTW: '
+                print msg,
                 diff = Diffusion(**base_kwds)
-                yield '', diff.to_graph()
+                yield diff.to_graph()
+            elif (impl is Implementation.OPENCL):
+                msg='   *OpenCl CLFFT: '
+                print msg
+                for cl_env in iter_clenv():
+                    msg='     |platform {}, device {}: '.format(cl_env.platform.name.strip(),
+                                                              cl_env.device.name.strip())
+                    print msg,
+                    sys.stdout.flush()
+                    diff = Diffusion(cl_env=cl_env, **base_kwds)
+                    yield diff.to_graph()
+                msg='   *OpenCl FFTW: '
+                print msg
+                cpu_envs = tuple(iter_clenv(device_type='cpu'))
+                if cpu_envs:
+                    for cl_env in cpu_envs:
+                        msg='     |platform {}, device {}'.format(cl_env.platform.name.strip(),
+                                                                  cl_env.device.name.strip())
+                        print msg,
+                        diff = Diffusion(cl_env=cl_env, enforce_implementation=False, **base_kwds)
+                        yield diff.to_graph()
             else:
                 msg='Unknown implementation to test {}.'.format(impl)
                 raise NotImplementedError(msg)
@@ -130,11 +150,24 @@ class TestDiffusionOperator(object):
         reference_fields = {}
         outputs = {}
 
+        print '\n >Testing all Implementations:'
         for impl in implementations:
-            print '\n >Implementation {}:'.format(impl),
-            for sop,op in iter_impl(impl):
-                if (sop != ''):
-                    print '\n   *{}: '.format(sop),
+            if (impl is Implementation.FORTRAN):
+                if ((dim not in (2,3))
+                      or ((dim==3) and (Fin.nb_components%3!=0))
+                      or (dtype != HYSOP_REAL)
+                      or any((bd != BoundaryCondition.PERIODIC) for bd in Fin.lboundaries)
+                      or any((bd != BoundaryCondition.PERIODIC) for bd in Fin.rboundaries)):
+                    print '   *Fortran FFTW: NO SUPPORT'
+                    continue
+            elif (impl is Implementation.OPENCL):
+                if (dim>3):
+                    print '   *OpenCl: NO SUPPORT'
+                    continue
+            for op in iter_impl(impl):
+                if (impl is ref_impl):
+                    print 'REF IMPL',
+                    sys.stdout.flush()
                 if (not is_inplace):
                     op.push_copy(dst=fin, src=fout)
                 op.build()
@@ -155,8 +188,6 @@ class TestDiffusionOperator(object):
 
                 view = dfin_c.compute_slices
 
-                sys.stdout.write('.')
-                sys.stdout.flush()
                 try:
                     dfout_c.initialize(self.__random_init, dtype=dfout.dtype)
                     dfin_c.initialize(self.__scalar_init, dtype=dfin.dtype)
@@ -193,7 +224,7 @@ class TestDiffusionOperator(object):
                             if not mask.all():
                                 msg='\nFATAL ERROR: Output is not finite on axis {}.\n'.format(i)
                                 print msg
-                                npw.fancy_print(output[i],
+                                npw.fancy_print(output[i], 
                                         replace_values={(lambda a: npw.isfinite(a)): '.'})
                                 raise RuntimeError(msg)
                             di = npw.abs(reference[i] - output[i])
@@ -215,7 +246,8 @@ class TestDiffusionOperator(object):
                                 msg+='\n > max tolerence was set to {} ({} eps).'
                                 msg=msg.format(i, max_di, max_tol, neps)
                                 raise RuntimeError(msg)
-
+                            sys.stdout.write('.')
+                            sys.stdout.flush()
                         S1 = dfout.integrate()
                         if not npw.all(npw.isfinite(S1)):
                             msg='Integral is not finite. Got {}.'.format(S1)
@@ -229,19 +261,21 @@ class TestDiffusionOperator(object):
                     sys.stdout.write('\bx\n\n')
                     sys.stdout.flush()
                     raise
+                print
 
 
-    # def test_diffusion_1D_inplace(self):
-    #     self._test(dim=1, is_inplace=True)
+    def test_diffusion_1D_inplace(self):
+        self._test(dim=1, is_inplace=True)
     def test_diffusion_2D_inplace(self):
         self._test(dim=2, is_inplace=True)
     def test_diffusion_3D_inplace(self):
         self._test(dim=3, is_inplace=True)
 
     def perform_tests(self):
-        #self.test_diffusion_1D_inplace()
+        self.test_diffusion_1D_inplace()
         self.test_diffusion_2D_inplace()
-        self.test_diffusion_3D_inplace()
+        if __ENABLE_LONG_TESTS__:
+            self.test_diffusion_3D_inplace()
         print
 
 
diff --git a/hysop/operator/tests/test_directional_advection.py b/hysop/operator/tests/test_directional_advection.py
index 13184b08800c0b90a5fb64996b1486b30bfc3653..9463d3745899f5668bdcf916e8a5a3e58d0d7449 100644
--- a/hysop/operator/tests/test_directional_advection.py
+++ b/hysop/operator/tests/test_directional_advection.py
@@ -217,11 +217,14 @@ class TestDirectionalAdvectionOperator(object):
                                 if npw.any(d > 1e-1):
                                     print 'FATAL ERROR: Could not match analytic advection.'
                                     print 'DSOUT'
-                                    print dsout.sdata[dsout.compute_slices]
+                                    for output in dsout:
+                                        print output.sdata[output.compute_slices]
                                     print 'DSREF'
-                                    print dsref.sdata[dsref.compute_slices]
+                                    for ref in dsref:
+                                        print ref.sdata[ref.compute_slices]
                                     print 'DSREF - DSOUT'
-                                    print (dsout.sdata[dsout.compute_slices].get() - dsref.sdata[dsref.compute_slices].get())
+                                    for (output, ref) in zip(dsout, dsref):
+                                        print (output.sdata[output.compute_slices].get() - ref.sdata[ref.compute_slices].get())
                                     msg='Test failed with V={}, k={}, dxk={}, inter-field L2 distances are {}.' 
                                     msg=msg.format(Vi, k, to_tuple(dxk, cast=float), to_tuple(d, cast=float))
                                     raise RuntimeError(msg)
diff --git a/hysop/operator/tests/test_directional_diffusion.py b/hysop/operator/tests/test_directional_diffusion.py
index a3ea8bb1c722bbc4bebef77ab553888766710c81..9d70d201c3c636e90371e5530616237684794585 100644
--- a/hysop/operator/tests/test_directional_diffusion.py
+++ b/hysop/operator/tests/test_directional_diffusion.py
@@ -12,7 +12,7 @@ from hysop.operator.directional.diffusion_dir import DirectionalDiffusion
 
 from hysop import Field, Box, Simulation
 from hysop.methods import Remesh, TimeIntegrator
-from hysop.constants import Implementation, DirectionLabels, ComputeGranularity, SpaceDiscretization
+from hysop.constants import Implementation, DirectionLabels, ComputeGranularity, SpaceDiscretization, HYSOP_REAL
 from hysop.numerics.splitting.strang import StrangSplitting, StrangOrder
 from hysop.numerics.remesh.remesh import RemeshKernel
 from hysop.numerics.stencil.stencil_generator import StencilGenerator, CenteredStencilGenerator, MPQ
@@ -54,11 +54,11 @@ class TestDirectionalDiffusionOperator(object):
 
         if self.enable_extra_tests:
             flt_types = (npw.float32, npw.float64)
-            time_integrators = (Euler, RK2,)
+            time_integrators = (RK2, RK4)
             orders = (2, 4, 6)
         else:
-            flt_types = (npw.float32,)
-            time_integrators = (Euler, RK2)
+            flt_types = (HYSOP_REAL,)
+            time_integrators = (RK2,)
             orders = (4,)
 
         domain = Box(length=(2*npw.pi,)*dim)
diff --git a/hysop/operator/tests/test_directional_stretching.py b/hysop/operator/tests/test_directional_stretching.py
index 8ec099254ff88bc0a740be77c990787d37ee4a4d..99f8006e251d9677312a3ee403957621932efb78 100644
--- a/hysop/operator/tests/test_directional_stretching.py
+++ b/hysop/operator/tests/test_directional_stretching.py
@@ -13,7 +13,7 @@ from hysop.parameters.scalar_parameter import ScalarParameter
 from hysop import Field, Box, Simulation
 from hysop.methods import Remesh, TimeIntegrator
 from hysop.constants import Implementation, DirectionLabels, ComputeGranularity, \
-                            SpaceDiscretization, StretchingFormulation
+                            SpaceDiscretization, StretchingFormulation, HYSOP_REAL
 from hysop.numerics.splitting.strang import StrangSplitting, StrangOrder
 from hysop.numerics.stencil.stencil_generator import StencilGenerator, CenteredStencilGenerator, \
                                                      MPQ
@@ -61,8 +61,8 @@ class TestDirectionalStretchingOperator(object):
             time_integrators = (Euler, RK2,)
             orders = (2, 4, 6)
         else:
-            flt_types = (npw.float32,)
-            time_integrators = (Euler, RK2)
+            flt_types = (HYSOP_REAL,)
+            time_integrators = (RK2,)
             orders = (4,)
 
         domain = Box(length=(2*npw.pi,)*dim)
diff --git a/hysop/operator/tests/test_derivative.py b/hysop/operator/tests/test_fd_derivative.py
similarity index 91%
rename from hysop/operator/tests/test_derivative.py
rename to hysop/operator/tests/test_fd_derivative.py
index 3cad6b885e727c5427e860a4dfb03997f14cfa09..334f8bed9b79800878d7b2c700bdd2d5f317d1bf 100644
--- a/hysop/operator/tests/test_derivative.py
+++ b/hysop/operator/tests/test_fd_derivative.py
@@ -12,20 +12,24 @@ from hysop.tools.numerics import is_fp, is_integer
 from hysop.tools.types import check_instance, first_not_None
 from hysop.tools.io_utils import IO
 from hysop.tools.numpywrappers import npw
+from hysop.tools.sympy_utils import round_expr
+from hysop.tools.spectral_utils import make_multivariate_trigonometric_polynomial, \
+                                       make_multivariate_polynomial
 from hysop.parameters.scalar_parameter import ScalarParameter
-from hysop.operator.derivative import Gradient, Implementation
+from hysop.operator.derivative import Implementation, FiniteDifferencesSpaceDerivative
+from hysop.operator.gradient import Gradient
 from hysop.operator.misc import ForceTopologyState
 
 from hysop import Field, Box
 
-class TestGradient(object):
+class TestFiniteDifferencesDerivative(object):
 
     @classmethod
     def setup_class(cls, 
             enable_extra_tests=__ENABLE_LONG_TESTS__,
             enable_debug_mode=False):
 
-        IO.set_default_path('/tmp/hysop_tests/test_gradient')
+        IO.set_default_path('/tmp/hysop_tests/test_fd_derivative')
         
         cls.size_min = 23
         cls.size_max = 35
@@ -142,7 +146,7 @@ class TestGradient(object):
         print ' >Parameter t has been set to {}.'.format(self.t())
         print ' >Testing all implementations:'
 
-        implementations = Gradient.implementations()
+        implementations = FiniteDifferencesSpaceDerivative.implementations()
        
         variables = { F:shape, gradF: shape }
         fns    = self.analytic_functions[dim]['F']
@@ -282,23 +286,27 @@ class TestGradient(object):
         self._test(dim=3, dtype=npw.float64)
 
     def perform_tests(self):
-        self.test_1d_float32()
-        self.test_2d_float32()
-        self.test_3d_float32()
+        if (HYSOP_REAL == npw.float32) or __ENABLE_LONG_TESTS__:
+            self.test_1d_float32()
+            self.test_2d_float32()
+            if __ENABLE_LONG_TESTS__:
+                self.test_3d_float32()
 
-        self.test_1d_float64()
-        self.test_2d_float64()
-        self.test_3d_float64()
+        if (HYSOP_REAL == npw.float64) or __ENABLE_LONG_TESTS__:
+            self.test_1d_float64()
+            self.test_2d_float64()
+            if __ENABLE_LONG_TESTS__:
+                self.test_3d_float64()
     
 if __name__ == '__main__':
-    TestGradient.setup_class(enable_extra_tests=False, 
+    TestFiniteDifferencesDerivative.setup_class(enable_extra_tests=False, 
                              enable_debug_mode=False)
     
-    test = TestGradient()
+    test = TestFiniteDifferencesDerivative()
 
     with printoptions(threshold=10000, linewidth=1000, 
                       nanstr='nan', infstr='inf', 
                       formatter={'float': lambda x: '{:>6.2f}'.format(x)}):
         test.perform_tests()
 
-    TestGradient.teardown_class()
+    TestFiniteDifferencesDerivative.teardown_class()
diff --git a/hysop/operator/tests/test_poisson.py b/hysop/operator/tests/test_poisson.py
index 7237d8841ee1af238254858806181546bee75563..d4cf58ea095dc926c07c905c3fc2d392fb5ebc41 100644
--- a/hysop/operator/tests/test_poisson.py
+++ b/hysop/operator/tests/test_poisson.py
@@ -1,13 +1,17 @@
 import random, primefac
 from hysop.deps import it, sm, random
-from hysop.constants import HYSOP_REAL
+from hysop.constants import HYSOP_REAL, BoundaryCondition
 from hysop.testsenv import __ENABLE_LONG_TESTS__, __HAS_OPENCL_BACKEND__
-from hysop.testsenv import opencl_failed, iter_clenv, test_context
+from hysop.testsenv import opencl_failed, iter_clenv, test_context, domain_boundary_iterator
+from hysop.tools.spectral_utils import make_multivariate_polynomial
 from hysop.tools.contexts import printoptions
 from hysop.tools.numerics import is_fp, is_integer
 from hysop.tools.types import check_instance, first_not_None
 from hysop.tools.io_utils import IO
 from hysop.tools.numpywrappers import npw
+from hysop.tools.sympy_utils import truncate_expr, round_expr
+from hysop.tools.spectral_utils import make_multivariate_trigonometric_polynomial, \
+                                       make_multivariate_polynomial
 from hysop.operator.poisson import Poisson, Implementation
 
 from hysop import Field, Box
@@ -21,61 +25,79 @@ class TestPoissonOperator(object):
 
         IO.set_default_path('/tmp/hysop_tests/test_poisson')
 
-        if enable_debug_mode:
-            cls.size_min = 15
-            cls.size_max = 16
-        else:
-            cls.size_min = 23
-            cls.size_max = 87
+        cls.size_min = 8
+        cls.size_max = 16
 
         cls.enable_extra_tests = enable_extra_tests
         cls.enable_debug_mode  = enable_debug_mode
-
-        cls.build_analytic_solutions()
-
-    @classmethod
-    def build_analytic_solutions(cls):
+        
         from hysop.tools.sympy_utils import enable_pretty_printing
-        from hysop.symbolic.base import TensorBase
-        from hysop.symbolic.frame import SymbolicFrame
-        from hysop.symbolic.field import curl, laplacian
         enable_pretty_printing()
 
-        #at this moment we just test a periodic solver
-
-        analytic_solutions = {}
-        analytic_functions = {}
-        for dim in (1,2,3,4):
-            frame = SymbolicFrame(dim=dim)
-            def gen_psi():
-                kis = tuple(random.randint(1,5) for _ in xrange(dim))
-                qis = tuple(npw.random.rand(dim).round(decimals=3).tolist())
-                basis = tuple( (sm.cos(ki*xi+qi), sm.sin(ki*xi+qi))
-                        for (ki,qi,xi) in zip(kis, qis, frame.coords) )
-                psi = sm.Integer(1)
-                for i in xrange(dim):
-                    psi *= random.choice(basis[i])
-                return psi
-            Psis = npw.asarray([gen_psi() for _ in xrange(1)], dtype=object).view(TensorBase)
-            Ws = laplacian(Psis, frame)
-            coords = frame.coords
-            fWs = tuple(sm.lambdify(coords, W) for W in Ws)
-            fPsis = tuple(sm.lambdify(coords, Psi) for Psi in Psis)
-            sols = {'Ws': Ws, 'Psis': Psis}
-            lambdified_sols = {'Ws': fWs, 'Psis': fPsis}
-            analytic_solutions[dim] = sols
-            analytic_functions[dim] = lambdified_sols
-        cls.analytic_solutions = analytic_solutions
-        cls.analytic_functions = analytic_functions
-
     @classmethod
     def teardown_class(cls):
         pass
 
+    @classmethod
+    def build_analytic_solutions(cls, polynomial, 
+                                      dim, nb_components,
+                                      lboundaries, rboundaries,
+                                      origin, end):
+        from hysop.symbolic.base  import TensorBase
+        from hysop.symbolic.frame import SymbolicFrame
+        from hysop.symbolic.field import laplacian
+        
+        frame = SymbolicFrame(dim=dim)
+        coords = frame.coords
+                
+        def gen_psi():
+            psis = ()
+            for i in xrange(nb_components):
+                if polynomial:
+                    psi, y = make_multivariate_polynomial(origin, end,
+                                                        lboundaries, rboundaries, 
+                                                        10, 4)
+                else:
+                    psi, y = make_multivariate_trigonometric_polynomial(origin, end,
+                            lboundaries, rboundaries, 2)
+                psi = psi.xreplace({yi: xi for (yi,xi) in zip(y, frame.coords)})
+                psis += (psi,)
+            return npw.asarray(psis).view(TensorBase)
+
+        Psis  = gen_psi()
+        Ws    = npw.atleast_1d(laplacian(Psis, frame))
+
+        fWs   = tuple(sm.lambdify(coords, W)   for W   in Ws)
+        fPsis = tuple(sm.lambdify(coords, Psi) for Psi in Psis)
+
+        analytic_expressions = {'Psi':Psis,  'W':Ws}
+        analytic_functions   = {'Psi':fPsis, 'W':fWs}
+        return (analytic_expressions, analytic_functions)
+
+    @staticmethod
+    def __random_init(data, coords, dtype):
+        for d in data:
+            if is_fp(d.dtype):
+                d[...] = npw.random.random(size=d.shape).astype(dtype=d.dtype)
+            else:
+                msg = 'Unknown dtype {}.'.format(d.dtype)
+                raise NotImplementedError(msg)
+
+    @staticmethod
+    def __analytic_init(data, coords, dtype, fns):
+        assert len(fns) == len(data)
+        for (d,fn,coord) in zip(data,fns,coords):
+            coord = tuple(c.astype(d.dtype) for c in coord)
+            d[...] = fn(*coord).astype(d.dtype)
 
-    def _test(self, dim, dtype,
-            size_min=None, size_max=None):
-        enable_extra_tests = self.enable_extra_tests
+
+    def _test(self, dim, dtype, max_runs=5,
+                   polynomial=False, size_min=None, size_max=None):
+
+        if (dtype == HYSOP_REAL):
+            nb_components = 1 # enable fortran poisson test
+        else:
+            nb_components = 2
 
         size_min = first_not_None(size_min, self.size_min)
         size_max = first_not_None(size_max, self.size_max)
@@ -86,53 +108,71 @@ class TestPoissonOperator(object):
             factors.clear()
             shape = tuple(npw.random.randint(low=size_min, high=size_max+1, size=dim).tolist())
             for Si in shape:
-                factors.update( set(primefac.primefac(int(Si-1))) )
-
-        domain = Box(length=(2*npw.pi,)*dim)
-        Psi = Field(domain=domain, name='Psi', dtype=dtype,
-                nb_components=1, register_object=False)
-        W = Field(domain=domain, name='W', dtype=dtype,
-                nb_components=1, register_object=False)
-
-        self._test_one(shape=shape, dim=dim, dtype=dtype,
-                domain=domain, Psi=Psi, W=W)
-        print
-
-    @classmethod
-    def __analytic_init(cls, data, coords, dtype, fns):
-        assert len(fns) == len(data)
-        for (d,fn,coord) in zip(data,fns,coords):
-            d[...] = fn(*coord).astype(dtype)
-
-    @staticmethod
-    def __random_init(data, coords, dtype):
-        shape = data[0].shape
-        if is_fp(dtype):
-            for d in data:
-                d[...] = npw.random.random(size=d.shape).astype(dtype=dtype)
+                factors.update( set(primefac.primefac(int(Si))) )
+       
+        domain_boundaries = list(domain_boundary_iterator(dim=dim))
+        periodic = domain_boundaries[0]
+        domain_boundaries = domain_boundaries[1:]
+        random.shuffle(domain_boundaries)
+        domain_boundaries.insert(0, periodic)
+
+        for i, (lboundaries, rboundaries) in enumerate(domain_boundaries, 1):
+            domain = Box(origin=(npw.random.rand(dim)-0.5),
+                         length=(npw.random.rand(dim)+0.5)*2*npw.pi,
+                            lboundaries=lboundaries,
+                            rboundaries=rboundaries)
+
+            Psi = Field(domain=domain, name='Psi', dtype=dtype,
+                    nb_components=nb_components, register_object=False)
+            W = Field(domain=domain, name='W', dtype=dtype,
+                    nb_components=nb_components, register_object=False)
+
+            self._test_one(shape=shape, dim=dim, dtype=dtype, 
+                    domain=domain, Psi=Psi, W=W,
+                    polynomial=polynomial, nb_components=nb_components)
+            if (max_runs is not None) and (i==max_runs):
+                missing = ((4**(dim+1) - 1) / 3) - i
+                print
+                print '>> MAX RUNS ACHIEVED FOR {}D DOMAINS -- SKIPING {} OTHER BOUNDARY CONDITIONS <<'.format(dim, missing)
+                print
+                print
+                break
         else:
-            msg = 'Unknown dtype {}.'.format(dtype)
-            raise NotImplementedError(msg)
+            assert (i==(4**(dim+1)-1)/3), (i+1, (4**(dim+1)-1)/3)
+            print
+            print '>> TESTED ALL {}D BOUNDARY CONDITIONS <<'.format(dim)
+            print
+            print
+
 
     def _test_one(self, shape, dim, dtype,
-            domain, Psi, W):
-
-        print '\nTesting periodic {}D Poisson: dtype={} shape={}'.format(
-                dim, dtype.__name__, shape)
-        print ' >Input analytic function:'
-        for (i,wi) in enumerate(self.analytic_solutions[dim]['Ws']):
-            print '  *W{} = {}'.format(i, wi)
-        print ' >Expected output solution:'
-        for (i,psii) in enumerate(self.analytic_solutions[dim]['Psis']):
-            print '  *Psi{} = {}'.format(i, psii)
-        print ' >Testing all implementations:'
+            domain, Psi, W, polynomial, nb_components):
+        
+        (analytic_expressions, analytic_functions) = \
+            self.build_analytic_solutions(
+                dim=dim, nb_components=nb_components, polynomial=polynomial, 
+                lboundaries=W.lboundaries[::-1], # => boundaries in variable order x0,...,xn
+                rboundaries=W.rboundaries[::-1],
+                origin=domain.origin[::-1],
+                end=domain.end[::-1])
+        
+        def format_expr(e):
+            return truncate_expr(round_expr(e, 3), 80)
+
+        msg='\nTesting {}D Poisson: dtype={} nb_components={} shape={} polynomial={}, bc=[{}]'.format(
+                dim, dtype.__name__, nb_components, shape, polynomial, W.domain.format_boundaries())
+        msg+='\n >Corresponding field boundary conditions are [{}].'.format(W.fields[0].format_boundaries())
+        msg+='\n >Input analytic functions are (truncated):'
+        for (Wi, Wis) in zip(W.fields, analytic_expressions['W']):
+            msg+='\n   *{}(x,t) = {}'.format(Wi.pretty_name, format_expr(Wis))
+        msg+='\n >Expected output solutions:'
+        for (Psi_i, Psis_i) in zip(Psi.fields, analytic_expressions['Psi']):
+            msg+='\n   *{}(x,t) = {}'.format(Psi_i.pretty_name, format_expr(Psis_i))
+        msg+='\n >Testing all implementations:'
+        print msg
 
         implementations = Poisson.implementations()
-
-        # Compute reference solution
-
         variables = { Psi:shape, W:shape }
-
         def iter_impl(impl):
             base_kwds = dict(Fout=Psi, Fin=W, variables=variables,
                              implementation=impl,
@@ -146,6 +186,7 @@ class TestPoissonOperator(object):
                 print msg,
                 yield Poisson(**base_kwds)
             elif impl is Implementation.OPENCL:
+                from hysop.backend.device.opencl import cl
                 msg='   *OpenCl CLFFT: '
                 print msg
                 for cl_env in iter_clenv():
@@ -153,31 +194,39 @@ class TestPoissonOperator(object):
                                                           cl_env.device.name.strip())
                     print msg,
                     yield Poisson(cl_env=cl_env, **base_kwds)
+                msg='   *OpenCl FFTW: '
+                print msg
+                cpu_envs = tuple(iter_clenv(device_type='cpu'))
+                if cpu_envs:
+                    for cl_env in cpu_envs:
+                        msg='     |platform {}, device {}'.format(cl_env.platform.name.strip(),
+                                                                  cl_env.device.name.strip())
+                        print msg,
+                        yield Poisson(cl_env=cl_env, enforce_implementation=False, **base_kwds)
             else:
                 msg='Unknown implementation to test {}.'.format(impl)
                 raise NotImplementedError(msg)
 
-        # Compare to analytic solution
+        #Compare to analytic solution
         Psiref = None
         Wref = None
         for impl in implementations:
-            if (dim>3) and (impl is Implementation.OPENCL):
-                print '   *OpenCl CLFFT: NO SUPPORT'
-                continue
             if (impl is Implementation.FORTRAN):
-                if ((dim<2) or (dim>3) or (not dtype is HYSOP_REAL)):
+                if ((nb_components>1) or (dim!=3) or (not dtype is HYSOP_REAL)
+                        or any((bd != BoundaryCondition.PERIODIC) for bd in W.lboundaries)
+                        or any((bd != BoundaryCondition.PERIODIC) for bd in W.rboundaries)):
                     print '   *Fortran FFTW: NO SUPPORT'
                     continue
             for op in iter_impl(impl):
-                op = op.build()
-                dw   = op.input_discrete_fields[W].as_contiguous_dfield()
-                dpsi = op.output_discrete_fields[Psi].as_contiguous_dfield()
+                op   = op.build()
+                dw   = op.get_input_discrete_field(W).as_contiguous_dfield()
+                dpsi = op.get_output_discrete_field(Psi).as_contiguous_dfield()
 
                 dw.initialize(self.__analytic_init, dtype=dtype,
-                        fns=self.analytic_functions[dim]['Ws'])
+                              fns=analytic_functions['W'])
                 if (Psiref is None):
                     dpsi.initialize(self.__analytic_init, dtype=dtype,
-                            fns=self.analytic_functions[dim]['Psis'])
+                            fns=analytic_functions['Psi'])
                     Wref   = tuple( data.get().handle.copy() for data in dw.data   )
                     Psiref = tuple( data.get().handle.copy() for data in dpsi.data )
                 dpsi.initialize(self.__random_init, dtype=dtype)
@@ -193,9 +242,9 @@ class TestPoissonOperator(object):
 
     @classmethod
     def _check_output(cls, impl, op, Wref, Psiref, Wout, Psiout):
-        check_instance(Wref, tuple,   values=npw.ndarray)
+        check_instance(Wref,   tuple, values=npw.ndarray)
         check_instance(Psiref, tuple, values=npw.ndarray, size=len(Wref))
-        check_instance(Wout, tuple,   values=npw.ndarray, size=len(Wref))
+        check_instance(Wout,   tuple, values=npw.ndarray, size=len(Wref))
         check_instance(Psiout, tuple, values=npw.ndarray, size=len(Wref))
 
         msg0 = 'Reference field {} is not finite.'
@@ -221,15 +270,20 @@ class TestPoissonOperator(object):
                 assert fout.flags.c_contiguous
                 assert fref.flags.c_contiguous
 
-                eps  = npw.finfo(fout.dtype).eps
-                dist = npw.abs(fout-fref)
-                dinf = npw.max(dist)
-                deps = int(npw.ceil(dinf/eps))
-                if (deps < 200):
-                    print '{}eps, '.format(deps),
-                    continue
                 has_nan = npw.any(npw.isnan(fout))
                 has_inf = npw.any(npw.isinf(fout))
+                if has_nan:
+                    deps = 'nan'
+                elif has_inf:
+                    deps = 'inf'
+                else:
+                    eps  = npw.finfo(fout.dtype).eps
+                    dist = npw.abs(fout-fref)
+                    dinf = npw.max(dist)
+                    deps = int(npw.ceil(dinf/eps))
+                if (deps < 10000):
+                    print '{}eps, '.format(deps),
+                    continue
 
                 print
                 print
@@ -270,39 +324,54 @@ class TestPoissonOperator(object):
                 raise RuntimeError(msg)
 
 
-    def test_1d_float32(self):
-        self._test(dim=1, dtype=npw.float32)
-    def test_2d_float32(self):
-        self._test(dim=2, dtype=npw.float32)
-    def test_3d_float32(self):
-        self._test(dim=3, dtype=npw.float32)
-    def test_4d_float32(self):
-        self._test(dim=4, dtype=npw.float32)
-
-    def test_1d_float64(self):
-        self._test(dim=1, dtype=npw.float64)
-    def test_2d_float64(self):
-        self._test(dim=2, dtype=npw.float64)
-    def test_3d_float64(self):
-        self._test(dim=3, dtype=npw.float64)
-    def test_4d_float64(self):
-        self._test(dim=4, dtype=npw.float64)
+    def test_1d_float32(self, **kwds):
+        self._test(dim=1, dtype=npw.float32, **kwds)
+    def test_2d_float32(self, **kwds):
+        self._test(dim=2, dtype=npw.float32, **kwds)
+    def test_3d_float32(self, **kwds):
+        self._test(dim=3, dtype=npw.float32, **kwds)
+    def test_4d_float32(self, **kwds):
+        self._test(dim=4, dtype=npw.float32, **kwds)
+
+    def test_1d_float64(self, **kwds):
+        self._test(dim=1, dtype=npw.float64, **kwds)
+    def test_2d_float64(self, **kwds):
+        self._test(dim=2, dtype=npw.float64, **kwds)
+    def test_3d_float64(self, **kwds):
+        self._test(dim=3, dtype=npw.float64, **kwds)
+    def test_4d_float64(self, **kwds):
+        self._test(dim=4, dtype=npw.float64, **kwds)
+    
+    def test_polynomial_1d_float32(self, **kwds):
+        self._test(dim=1, dtype=npw.float32, polynomial=True, **kwds)
+    def test_polynomial_2d_float32(self, **kwds):
+        self._test(dim=2, dtype=npw.float32, polynomial=True, **kwds)
+    def test_polynomial_3d_float32(self, **kwds):
+        self._test(dim=3, dtype=npw.float32, polynomial=True, **kwds)
 
 
     def perform_tests(self):
-        self.test_1d_float32()
-        self.test_2d_float32()
-        self.test_3d_float32()
-        self.test_4d_float32()
-
-        self.test_1d_float64()
-        self.test_2d_float64()
-        self.test_3d_float64()
-        self.test_4d_float64()
+        max_1d_runs = None
+        max_2d_runs = 2
+        max_3d_runs = 2
+        max_4d_runs = 2
+
+        if __ENABLE_LONG_TESTS__ or (HYSOP_REAL==npw.float32):
+            self.test_1d_float32(max_runs=max_1d_runs)
+            self.test_2d_float32(max_runs=max_2d_runs)
+            if __ENABLE_LONG_TESTS__:
+                self.test_3d_float32(max_runs=max_3d_runs)
+                self.test_4d_float32(max_runs=max_4d_runs)
+        if __ENABLE_LONG_TESTS__ or (HYSOP_REAL==npw.float64):
+            self.test_1d_float64(max_runs=max_1d_runs)
+            self.test_2d_float64(max_runs=max_2d_runs)
+            if __ENABLE_LONG_TESTS__:
+                self.test_3d_float64(max_runs=max_3d_runs)
+                self.test_4d_float32(max_runs=max_4d_runs)
 
 if __name__ == '__main__':
     TestPoissonOperator.setup_class(enable_extra_tests=False,
-                                      enable_debug_mode=False)
+                                    enable_debug_mode=False)
 
     test = TestPoissonOperator()
 
diff --git a/hysop/operator/tests/test_poisson_curl.py b/hysop/operator/tests/test_poisson_curl.py
new file mode 100644
index 0000000000000000000000000000000000000000..33e779441918ce96aa44add9969a752c5ef8bb09
--- /dev/null
+++ b/hysop/operator/tests/test_poisson_curl.py
@@ -0,0 +1,363 @@
+# coding: utf-8
+
+import random, primefac
+from hysop.deps import it, sm, random
+from hysop.constants import HYSOP_REAL, BoxBoundaryCondition, BoundaryCondition
+from hysop.defaults import VelocityField, VorticityField
+from hysop.testsenv import __ENABLE_LONG_TESTS__, __HAS_OPENCL_BACKEND__
+from hysop.testsenv import opencl_failed, iter_clenv, test_context, domain_boundary_iterator
+from hysop.tools.contexts import printoptions
+from hysop.tools.numerics import is_fp, is_integer
+from hysop.tools.types import check_instance, first_not_None
+from hysop.tools.io_utils import IO
+from hysop.tools.numpywrappers import npw
+from hysop.operator.poisson_curl import PoissonCurl, Implementation
+from hysop.tools.sympy_utils import truncate_expr, round_expr
+from hysop.tools.spectral_utils import make_multivariate_trigonometric_polynomial, \
+                                       make_multivariate_polynomial
+
+from hysop import Field, Box
+
+class TestPoissonCurlOperator(object):
+
+    @classmethod
+    def setup_class(cls,
+            enable_extra_tests=__ENABLE_LONG_TESTS__,
+            enable_debug_mode=False):
+
+        IO.set_default_path('/tmp/hysop_tests/test_poisson_curl')
+
+
+        cls.size_min = 8
+        cls.size_max = 16
+
+        cls.enable_extra_tests = enable_extra_tests
+        cls.enable_debug_mode  = enable_debug_mode
+        
+        from hysop.tools.sympy_utils import enable_pretty_printing
+        enable_pretty_printing()
+    
+    @classmethod
+    def teardown_class(cls):
+        pass
+
+
+    @classmethod
+    def build_analytic_solutions(cls, polynomial, 
+                                      dim, nb_components,
+                                      lboundaries, rboundaries,
+                                      origin, end):
+        from hysop.symbolic.base  import TensorBase
+        from hysop.symbolic.frame import SymbolicFrame
+        from hysop.symbolic.field import laplacian, curl
+
+        assert len(lboundaries)==nb_components
+        assert len(rboundaries)==nb_components
+        
+        frame = SymbolicFrame(dim=dim)
+        coords = frame.coords
+        def gen_psi():
+            psis = ()
+            for i in xrange(nb_components):
+                if polynomial:
+                    psi, y = make_multivariate_polynomial(origin, end,
+                                                        lboundaries[i], rboundaries[i], 
+                                                        10, 4)
+                else:
+                    psi, y = make_multivariate_trigonometric_polynomial(origin, end,
+                            lboundaries[i], rboundaries[i], 2)
+                psi = psi.xreplace({yi: xi for (yi,xi) in zip(y, coords)})
+                psis += (psi,)
+            return npw.asarray(psis).view(TensorBase)
+        
+        Psis  = gen_psi()
+        Ws    = npw.atleast_1d(-laplacian(Psis, frame))
+        Us    = curl(Psis, frame)
+
+        fPsis = tuple(sm.lambdify(coords, Psi) for Psi in Psis)
+        fWs   = tuple(sm.lambdify(coords, W) for W in Ws)
+        fUs   = tuple(sm.lambdify(coords, U) for U in Us)
+
+        analytic_expressions = {'Psi':Psis,  'W':Ws,  'U':Us}
+        analytic_functions   = {'Psi':fPsis, 'W':fWs, 'U':fUs}
+        return (analytic_expressions, analytic_functions)
+
+    
+    @staticmethod
+    def __random_init(data, coords, dtype):
+        for d in data:
+            if is_fp(d.dtype):
+                d[...] = npw.random.random(size=d.shape).astype(dtype=d.dtype)
+            else:
+                msg = 'Unknown dtype {}.'.format(d.dtype)
+                raise NotImplementedError(msg)
+    
+    @staticmethod
+    def __analytic_init(data, coords, dtype, fns):
+        assert len(fns) == len(data)
+        for (d,fn,coord) in zip(data,fns,coords):
+            coord = tuple(c.astype(d.dtype) for c in coord)
+            d[...] = fn(*coord).astype(d.dtype)
+
+
+
+    def _test(self, dim, dtype, max_runs=5,
+            polynomial=False, size_min=None, size_max=None):
+        enable_extra_tests = self.enable_extra_tests
+
+        size_min = first_not_None(size_min, self.size_min)
+        size_max = first_not_None(size_max, self.size_max)
+
+        valid_factors = {2,3,5,7,11,13}
+        factors = {1}
+        while (factors-valid_factors):
+            factors.clear()
+            shape = tuple(npw.random.randint(low=size_min, high=size_max+1, size=dim).tolist())
+            for Si in shape:
+                factors.update( set(primefac.primefac(int(Si))) )
+        
+        domain_boundaries = list(domain_boundary_iterator(dim=dim))
+        periodic = domain_boundaries[0]
+        domain_boundaries = domain_boundaries[1:]
+        random.shuffle(domain_boundaries)
+        domain_boundaries.insert(0, periodic)
+
+        for i, (lboundaries, rboundaries) in enumerate(domain_boundaries, 1):
+            domain = Box(origin=(npw.random.rand(dim)-0.5),
+                         length=(0.5+npw.random.rand(dim))*2*npw.pi,
+                         lboundaries=lboundaries,
+                         rboundaries=rboundaries)
+            U = VelocityField(domain=domain, dtype=dtype)
+            W = VorticityField(velocity=U, dtype=dtype)
+
+            self._test_one(shape=shape, dim=dim, dtype=dtype, 
+                    domain=domain, W=W, U=U, polynomial=polynomial)
+            if (max_runs is not None) and (i==max_runs):
+                missing = ((4**(dim+1) - 1) / 3) - i
+                print
+                print '>> MAX RUNS ACHIEVED FOR {}D DOMAINS -- SKIPING {} OTHER BOUNDARY CONDITIONS <<'.format(dim, missing)
+                print
+                print
+                break
+        else:
+            assert (i==(4**(dim+1)-1)/3), (i+1, (4**(dim+1)-1)/3)
+            print
+            print '>> TESTED ALL {}D BOUNDARY CONDITIONS <<'.format(dim)
+            print
+            print
+
+    def _test_one(self, shape, dim, dtype,
+            domain, U, W, polynomial):
+        
+        (analytic_expressions, analytic_functions) = \
+            self.build_analytic_solutions(
+                dim=dim, nb_components=W.nb_components, polynomial=polynomial, 
+                lboundaries=[Wi.lboundaries[::-1] for Wi in W.fields], # => boundaries in variable order x0,...,xn
+                rboundaries=[Wi.rboundaries[::-1] for Wi in W.fields],
+                origin=domain.origin[::-1],
+                end=domain.end[::-1])
+        
+        def format_expr(e):
+            return truncate_expr(round_expr(e, 3), 80)
+
+        msg='\nTesting {}D PoissonCurl: dtype={} shape={} polynomial={}, bc=[{}]'.format(
+                dim, dtype.__name__, shape, polynomial, domain.format_boundaries())
+        print msg
+        print ' >Input analytic vorticity is (truncated):'
+        for (Wi, Wis) in zip(W.fields, analytic_expressions['W']):
+            print '  *{}(x) = {}'.format(Wi.pretty_name, format_expr(Wis))
+        print ' >Expected output velocity is:'
+        for (Ui, Uis) in zip(U.fields, analytic_expressions['U']):
+            print '  *{}(x) = {}'.format(Ui.pretty_name, format_expr(Uis))
+        print ' >Testing all implementations:'
+
+        implementations = PoissonCurl.implementations().keys()
+        variables = { U:shape, W:shape }
+
+        def iter_impl(impl):
+            base_kwds = dict(velocity=U, vorticity=W, variables=variables,
+                             implementation=impl,
+                             name='poisson_{}'.format(str(impl).lower()))
+            if impl is Implementation.FORTRAN:
+                msg='   *Fortran FFTW: '
+                print msg,
+                yield PoissonCurl(**base_kwds)
+            elif impl is Implementation.PYTHON:
+                msg='   *Python FFTW: '
+                print msg,
+                yield PoissonCurl(**base_kwds)
+            elif impl is Implementation.OPENCL:
+                msg='   *OpenCl CLFFT: '
+                print msg
+                for cl_env in iter_clenv():
+                    msg='     |platform {}, device {}'.format(cl_env.platform.name.strip(),
+                                                          cl_env.device.name.strip())
+                    print msg,
+                    yield PoissonCurl(cl_env=cl_env, projection=0, **base_kwds)
+                msg='   *OpenCl FFTW: '
+                print msg
+                cpu_envs = tuple(iter_clenv(device_type='cpu'))
+                if cpu_envs:
+                    for cl_env in cpu_envs:
+                        msg='     |platform {}, device {}'.format(cl_env.platform.name.strip(),
+                                                                  cl_env.device.name.strip())
+                        print msg,
+                        yield PoissonCurl(cl_env=cl_env, enforce_implementation=False, **base_kwds)
+            else:
+                msg='Unknown implementation to test {}.'.format(impl)
+                raise NotImplementedError(msg)
+
+        # Compare to analytic solution
+        Uref = None
+        Wref = None
+        for impl in implementations:
+            if (impl is Implementation.FORTRAN) and ((dtype != HYSOP_REAL)
+                     or any((bd != BoxBoundaryCondition.PERIODIC) for bd in domain.lboundaries)
+                     or any((bd != BoxBoundaryCondition.PERIODIC) for bd in domain.rboundaries)):
+                    print '   *Fortran FFTW: NO SUPPORT'
+                    continue
+            for (i,op) in enumerate(iter_impl(impl)):
+                from hysop.tools.debug_dumper import DebugDumper
+                name='{}_{}'.format(impl, i)
+
+                op = op.build()
+                
+                dw = op.get_input_discrete_field(W).as_contiguous_dfield()
+                du = op.get_output_discrete_field(U).as_contiguous_dfield()
+                
+                dw.initialize(self.__analytic_init, dtype=dtype,
+                                    fns=analytic_functions['W'])
+
+                if (Uref is None):
+                    du.initialize(self.__analytic_init, dtype=dtype,
+                                    fns=analytic_functions['U'])
+                    Wref = tuple( data.get().handle.copy() for data in dw.data )
+                    Uref = tuple( data.get().handle.copy() for data in du.data )
+                du.initialize(self.__random_init, dtype=dtype)
+
+                op.apply(simulation=None)
+
+                Wout = tuple( data.get().handle.copy() for data in dw.data )
+                Uout = tuple( data.get().handle.copy() for data in du.data )
+                self._check_output(impl, op, Wref, Uref, Wout, Uout)
+                print
+
+    @classmethod
+    def _check_output(cls, impl, op, Wref, Uref, Wout, Uout):
+        check_instance(Wref, tuple, values=npw.ndarray)
+        check_instance(Uref, tuple, values=npw.ndarray)
+        check_instance(Wout, tuple, values=npw.ndarray, size=len(Wref))
+        check_instance(Uout, tuple, values=npw.ndarray, size=len(Uref))
+
+        msg0 = 'Reference field {} is not finite.'
+        for (fields, name) in zip((Wref, Uref),('Wref', 'Uref')):
+            for (i,field) in enumerate(fields):
+                iname = '{}{}'.format(name,i)
+                mask = npw.isfinite(field)
+                if not mask.all():
+                    print
+                    print field
+                    print
+                    print field[~mask]
+                    print
+                    msg = msg0.format(iname)
+                    raise ValueError(msg)
+
+        for (out_buffers, ref_buffers, name) in zip((Wout, Uout), (Wref, Uref), ('W', 'U')):
+            for i, (fout,fref) in enumerate(zip(out_buffers, ref_buffers)):
+                iname = '{}{}'.format(name,i)
+                assert fout.dtype == fref.dtype, iname
+                assert fout.shape == fref.shape, iname
+                assert not npw.any(npw.isnan(fref))
+                assert not npw.any(npw.isinf(fref))
+
+                has_nan = npw.any(npw.isnan(fout))
+                has_inf = npw.any(npw.isinf(fout))
+                if has_nan:
+                    deps = 'nan'
+                elif has_inf:
+                    deps = 'inf'
+                else:
+                    eps  = npw.finfo(fout.dtype).eps
+                    dist = npw.abs(fout-fref)
+                    dinf = npw.max(dist)
+                    try:
+                        deps = int(npw.ceil(dinf/eps))
+                    except:
+                        deps = 'inf'
+                if (deps < 10000):
+                    print '{}eps, '.format(deps),
+                    continue
+
+                print
+                print
+                print 'Test output comparisson for {} failed for component {}:'.format(name, i)
+                print ' *has_nan: {}'.format(has_nan)
+                print ' *has_inf: {}'.format(has_inf)
+                print ' *dinf={} ({} eps)'.format(dinf, deps)
+                print
+                if cls.enable_debug_mode:
+                    print 'REFERENCE INPUTS:'
+                    for (i,w) in enumerate(Wref):
+                        print 'W{}'.format(i)
+                        print w
+                        print
+                    if (name == 'U'):
+                        print 'REFERENCE OUTPUT:'
+                        for (i,u) in enumerate(Uref):
+                            print 'U{}'.format(i)
+                            print u
+                            print
+                        print
+                        print 'OPERATOR {} OUTPUT:'.format(op.name.upper())
+                        print
+                        for (i,u) in enumerate(Uout):
+                            print 'U{}'.format(i)
+                            print u
+                            print
+                    else:
+                        print 'MODIFIED INPUTS:'
+                        for (i,w) in enumerate(Wout):
+                            print 'W{}'.format(i)
+                            print w
+                            print
+                    print
+
+                msg = 'Test failed for {} on component {} for implementation {}.'
+                msg = msg.format(name, i, impl)
+                raise RuntimeError(msg)
+
+
+    def test_2d_float32(self, **kwds):
+        self._test(dim=2, dtype=npw.float32, **kwds)
+    def test_3d_float32(self, **kwds):
+        self._test(dim=3, dtype=npw.float32, **kwds)
+
+    def test_2d_float64(self, **kwds):
+        self._test(dim=2, dtype=npw.float64, **kwds)
+    def test_3d_float64(self, **kwds):
+        self._test(dim=3, dtype=npw.float64, **kwds)
+
+    def perform_tests(self):
+        max_2d_runs = None if __ENABLE_LONG_TESTS__ else 2
+        max_3d_runs = None if __ENABLE_LONG_TESTS__ else 2
+
+        if __ENABLE_LONG_TESTS__ or (HYSOP_REAL==npw.float32):
+            self.test_2d_float32(max_runs=max_2d_runs)
+            self.test_3d_float32(max_runs=max_3d_runs)
+        if __ENABLE_LONG_TESTS__ or (HYSOP_REAL==npw.float64):
+            self.test_2d_float64(max_runs=max_2d_runs)
+            self.test_3d_float64(max_runs=max_3d_runs)
+
+if __name__ == '__main__':
+    TestPoissonCurlOperator.setup_class(enable_extra_tests=False,
+                                        enable_debug_mode=False)
+
+    test = TestPoissonCurlOperator()
+
+    with printoptions(threshold=10000, linewidth=240,
+                      nanstr='nan', infstr='inf',
+                      formatter={'float': lambda x: '{:>6.2f}'.format(x)}):
+        test.perform_tests()
+
+    TestPoissonCurlOperator.teardown_class()
diff --git a/hysop/operator/tests/test_poisson_rotational.py b/hysop/operator/tests/test_poisson_rotational.py
deleted file mode 100644
index 6d405fc4e1439044a1fea0909edd41ab93d8bb32..0000000000000000000000000000000000000000
--- a/hysop/operator/tests/test_poisson_rotational.py
+++ /dev/null
@@ -1,307 +0,0 @@
-# coding: utf-8
-
-import random, primefac
-from hysop.deps import it, sm, random
-from hysop.constants import HYSOP_REAL
-from hysop.testsenv import __ENABLE_LONG_TESTS__, __HAS_OPENCL_BACKEND__
-from hysop.testsenv import opencl_failed, iter_clenv
-from hysop.tools.contexts import printoptions
-from hysop.tools.numerics import is_fp, is_integer
-from hysop.tools.types import check_instance, first_not_None
-from hysop.tools.io_utils import IO
-from hysop.tools.numpywrappers import npw
-from hysop.operator.poisson_rotational import PoissonRotational, Implementation
-
-from hysop import Field, Box
-
-class TestPoissonRotationalOperator(object):
-
-    @classmethod
-    def setup_class(cls,
-            enable_extra_tests=__ENABLE_LONG_TESTS__,
-            enable_debug_mode=False):
-
-        IO.set_default_path('/tmp/hysop_tests/test_poisson_rotational')
-
-        if enable_debug_mode:
-            cls.size_min = 5
-            cls.size_max = 5
-        else:
-            cls.size_min = 53
-            cls.size_max = 87
-
-        cls.enable_extra_tests = enable_extra_tests
-        cls.enable_debug_mode  = enable_debug_mode
-
-        cls.build_analytic_solutions()
-
-    @classmethod
-    def build_analytic_solutions(cls):
-        from hysop.tools.sympy_utils import enable_pretty_printing
-        from hysop.symbolic.base import TensorBase
-        from hysop.symbolic.frame import SymbolicFrame
-        from hysop.symbolic.field import curl, laplacian
-        enable_pretty_printing()
-
-        #at this moment we just test a periodic solver
-        frame = SymbolicFrame(dim=3)
-        def gen_psi():
-            kis = tuple(random.randint(1,5) for _ in xrange(3))
-            qis = tuple(npw.random.rand(3).round(decimals=3).tolist())
-            basis = tuple( (sm.cos(ki*xi+qi), sm.sin(ki*xi+qi)) \
-                            for (ki,qi,xi) in zip(kis, qis, frame.coords) )
-            psi = sm.Integer(1)
-            for i in xrange(dim):
-                psi *= random.choice(basis[i])
-            return psi
-
-        analytic_solutions = {}
-        analytic_functions = {}
-        for dim in (2,3):
-            psis = npw.asarray([gen_psi() for _ in xrange(3)],
-                                        dtype=object).view(TensorBase)
-            if (dim==2):
-                psis[:2] = 0
-            Ws = -laplacian(psis, frame)
-            Us = curl(psis, frame)
-            coords = frame.coords
-            if (dim==2):
-                Us = Us[:2]
-                Ws = Ws[-1:]
-                psis = psis[-1:]
-                coords = coords[:-1]
-
-            fWs = tuple(sm.lambdify(coords, W) for W in Ws)
-            fUs = tuple(sm.lambdify(coords, U) for U in Us)
-
-            sols = {'Ws': Ws, 'Us': Us}
-            lambdified_sols = {'Ws': fWs, 'Us': fUs}
-            analytic_solutions[dim] = sols
-            analytic_functions[dim] = lambdified_sols
-        cls.analytic_solutions = analytic_solutions
-        cls.analytic_functions = analytic_functions
-
-    @classmethod
-    def teardown_class(cls):
-        pass
-
-
-    def _test(self, dim, dtype,
-            size_min=None, size_max=None):
-        enable_extra_tests = self.enable_extra_tests
-
-        size_min = first_not_None(size_min, self.size_min)
-        size_max = first_not_None(size_max, self.size_max)
-
-        valid_factors = {2,3,5,7,11,13}
-        factors = {1}
-        while (factors-valid_factors):
-            factors.clear()
-            shape = tuple(npw.random.randint(low=size_min, high=size_max+1, size=dim).tolist())
-            for Si in shape:
-                factors.update( set(primefac.primefac(int(Si-1))) )
-
-        domain = Box(length=(2*npw.pi,)*dim)
-        U = Field(domain=domain, name='U', dtype=dtype,
-                nb_components=dim, register_object=False)
-        W = Field(domain=domain, name='W', dtype=dtype,
-                nb_components=(3 if (dim==3) else 1), register_object=False)
-
-        self._test_one(shape=shape, dim=dim, dtype=dtype,
-                       domain=domain, U=U, W=W)
-        print
-
-    @classmethod
-    def __analytic_init(cls, data, coords, dtype, fns):
-        assert len(fns) == len(data) == len(coords)
-        for (d,fn,coord) in zip(data,fns,coords):
-            d[...] = npw.asarray(fn(*coord)).astype(dtype)
-
-    @staticmethod
-    def __random_init(data, coords, dtype):
-        if is_fp(dtype):
-            for d in data:
-                d[...] = npw.random.random(size=d.shape).astype(dtype=dtype)
-        else:
-            msg = 'Unknown dtype {}.'.format(dtype)
-            raise NotImplementedError(msg)
-
-    def _test_one(self, shape, dim, dtype,
-            domain, U, W):
-
-        print '\nTesting periodic {}D PoissonRotational: dtype={} shape={}'.format(
-                dim, dtype.__name__, shape)
-        print ' >Input analytic vorticity:'
-        for (i,wi) in enumerate(self.analytic_solutions[dim]['Ws']):
-            print '  *W{} = {}'.format(i, wi)
-        print ' >Expected output velocity is:'
-        for (i,ui) in enumerate(self.analytic_solutions[dim]['Us']):
-            print '  *U{} = {}'.format(i, ui)
-        print ' >Testing all implementations:'
-
-        implementations = PoissonRotational.implementations().keys()
-        variables = { U:shape, W:shape }
-
-        def iter_impl(impl):
-            base_kwds = dict(velocity=U, vorticity=W, variables=variables,
-                             implementation=impl,
-                             name='poisson_{}'.format(str(impl).lower()))
-            if impl is Implementation.FORTRAN:
-                msg='   *Fortran FFTW: '
-                print msg,
-                yield PoissonRotational(**base_kwds)
-            elif impl is Implementation.PYTHON:
-                msg='   *Python FFTW: '
-                print msg,
-                yield PoissonRotational(**base_kwds)
-            elif impl is Implementation.OPENCL:
-                msg='   *OpenCl CLFFT: '
-                print msg
-                for cl_env in iter_clenv():
-                    msg='     |platform {}, device {}'.format(cl_env.platform.name.strip(),
-                                                          cl_env.device.name.strip())
-                    print msg,
-                    yield PoissonRotational(cl_env=cl_env, projection=0, **base_kwds)
-            else:
-                msg='Unknown implementation to test {}.'.format(impl)
-                raise NotImplementedError(msg)
-
-        # Compare to analytic solution
-        Uref = None
-        Wref = None
-        for impl in implementations:
-            if (impl is Implementation.FORTRAN) and not dtype is HYSOP_REAL:
-                print '   *FORTRAN: NO SUPPORT for ' + str(dtype)
-                continue
-            for (i,op) in enumerate(iter_impl(impl)):
-                from hysop.tools.debug_dumper import DebugDumper
-                name='{}_{}'.format(impl, i)
-                dbg = DebugDumper(name, force_overwrite=True)
-
-                op = op.build()
-                
-                dw = op.get_input_discrete_field(W).as_contiguous_dfield()
-                du = op.get_output_discrete_field(U).as_contiguous_dfield()
-                
-                dw.initialize(self.__analytic_init, dtype=dtype,
-                                    fns=self.analytic_functions[dim]['Ws'])
-
-                if (Uref is None):
-                    du.initialize(self.__analytic_init, dtype=dtype,
-                                    fns=self.analytic_functions[dim]['Us'])
-                    Wref = tuple( data.get().handle.copy() for data in dw.data )
-                    Uref = tuple( data.get().handle.copy() for data in du.data )
-                du.initialize(self.__random_init, dtype=dtype)
-
-                op.apply(simulation=None, debug_dumper=dbg)
-
-                Wout = tuple( data.get().handle.copy() for data in dw.data )
-                Uout = tuple( data.get().handle.copy() for data in du.data )
-                self._check_output(impl, op, Wref, Uref, Wout, Uout)
-                print
-
-    @classmethod
-    def _check_output(cls, impl, op, Wref, Uref, Wout, Uout):
-        check_instance(Wref, tuple, values=npw.ndarray)
-        check_instance(Uref, tuple, values=npw.ndarray)
-        check_instance(Wout, tuple, values=npw.ndarray, size=len(Wref))
-        check_instance(Uout, tuple, values=npw.ndarray, size=len(Uref))
-
-        msg0 = 'Reference field {} is not finite.'
-        for (fields, name) in zip((Wref, Uref),('Wref', 'Uref')):
-            for (i,field) in enumerate(fields):
-                iname = '{}{}'.format(name,i)
-                mask = npw.isfinite(field)
-                if not mask.all():
-                    print
-                    print field
-                    print
-                    print field[~mask]
-                    print
-                    msg = msg0.format(iname)
-                    raise ValueError(msg)
-
-        for (out_buffers, ref_buffers, name) in zip((Wout, Uout), (Wref, Uref), ('W', 'U')):
-            for i, (fout,fref) in enumerate(zip(out_buffers, ref_buffers)):
-                iname = '{}{}'.format(name,i)
-                assert fout.dtype == fref.dtype, iname
-                assert fout.shape == fref.shape, iname
-
-                eps  = npw.finfo(fout.dtype).eps
-                dist = npw.abs(fout-fref)
-                dinf = npw.max(dist)
-                deps = int(npw.ceil(dinf/eps))
-                if (deps < 500):
-                    print '{}eps, '.format(deps),
-                    continue
-                has_nan = npw.any(npw.isnan(fout))
-                has_inf = npw.any(npw.isinf(fout))
-
-                print
-                print
-                print 'Test output comparisson for {} failed for component {}:'.format(name, i)
-                print ' *has_nan: {}'.format(has_nan)
-                print ' *has_inf: {}'.format(has_inf)
-                print ' *dinf={} ({} eps)'.format(dinf, deps)
-                print
-                if cls.enable_debug_mode:
-                    print 'REFERENCE INPUTS:'
-                    for (i,w) in enumerate(Wref):
-                        print 'W{}'.format(i)
-                        print w
-                        print
-                    if (name == 'U'):
-                        print 'REFERENCE OUTPUT:'
-                        for (i,u) in enumerate(Uref):
-                            print 'U{}'.format(i)
-                            print u
-                            print
-                        print
-                        print 'OPERATOR {} OUTPUT:'.format(op.name.upper())
-                        print
-                        for (i,u) in enumerate(Uout):
-                            print 'U{}'.format(i)
-                            print u
-                            print
-                    else:
-                        print 'MODIFIED INPUTS:'
-                        for (i,w) in enumerate(Wout):
-                            print 'W{}'.format(i)
-                            print w
-                            print
-                    print
-
-                msg = 'Test failed for {} on component {} for implementation {}.'
-                msg = msg.format(name, i, impl)
-                raise RuntimeError(msg)
-
-
-    def test_2d_float32(self):
-        self._test(dim=2, dtype=npw.float32)
-    def test_3d_float32(self):
-        self._test(dim=3, dtype=npw.float32)
-
-    def test_2d_float64(self):
-        self._test(dim=2, dtype=npw.float64)
-    def test_3d_float64(self):
-        self._test(dim=3, dtype=npw.float64)
-
-    def perform_tests(self):
-        self.test_2d_float32()
-        self.test_3d_float32()
-
-        self.test_2d_float64()
-        self.test_3d_float64()
-
-if __name__ == '__main__':
-    TestPoissonRotationalOperator.setup_class(enable_extra_tests=False,
-                                      enable_debug_mode=False)
-
-    test = TestPoissonRotationalOperator()
-
-    with printoptions(threshold=10000, linewidth=240,
-                      nanstr='nan', infstr='inf',
-                      formatter={'float': lambda x: '{:>6.2f}'.format(x)}):
-        test.perform_tests()
-
-    TestPoissonRotationalOperator.teardown_class()
diff --git a/hysop/operator/tests/test_solenoidal_projection.py b/hysop/operator/tests/test_solenoidal_projection.py
index aeba561460505697e6226a805c73d9e33b729ca0..49da261b081919cedf36884e4f156c74a8ec1664 100644
--- a/hysop/operator/tests/test_solenoidal_projection.py
+++ b/hysop/operator/tests/test_solenoidal_projection.py
@@ -1,15 +1,19 @@
 
-import random, primefac
-from hysop.deps import it, sm, random
-from hysop.constants import HYSOP_REAL
+import random, primefac, scipy
+from hysop.deps import it, sm, random, np
+from hysop.constants import HYSOP_REAL, Implementation, BoxBoundaryCondition
+from hysop.defaults import VelocityField, VorticityField
 from hysop.testsenv import __ENABLE_LONG_TESTS__, __HAS_OPENCL_BACKEND__
-from hysop.testsenv import opencl_failed, iter_clenv
+from hysop.testsenv import opencl_failed, iter_clenv, test_context, domain_boundary_iterator
 from hysop.tools.contexts import printoptions
 from hysop.tools.numerics import is_fp, is_integer
-from hysop.tools.types import check_instance, first_not_None
+from hysop.tools.types import check_instance, first_not_None, to_list, to_tuple
 from hysop.tools.io_utils import IO
 from hysop.tools.numpywrappers import npw
 from hysop.operator.solenoidal_projection import SolenoidalProjection, Implementation
+from hysop.tools.sympy_utils import truncate_expr, round_expr
+from hysop.tools.spectral_utils import make_multivariate_trigonometric_polynomial, \
+                                       make_multivariate_polynomial
 
 from hysop import Field, Box
 
@@ -22,115 +26,130 @@ class TestSolenoidalProjectionOperator(object):
 
         IO.set_default_path('/tmp/hysop_tests/test_solenoidal_projection')
         
-        # /!\ warning in debug mode there is not enough points
-        #     to compute div(Uin)
-        if enable_debug_mode: 
-            cls.size_min = 4 
-            cls.size_max = 4
-        else:
-            cls.size_min = 100
-            cls.size_max = 150
+        cls.size_min = 8
+        cls.size_max = 16
         
         cls.enable_extra_tests = enable_extra_tests
         cls.enable_debug_mode  = enable_debug_mode
-
-    def build_analytic_solutions(self):
+        
         from hysop.tools.sympy_utils import enable_pretty_printing
+        enable_pretty_printing()
+
+    def build_analytic_solutions(self, polynomial,
+                    lboundaries, rboundaries, origin, end):
         from hysop.symbolic.base import TensorBase
         from hysop.symbolic.frame import SymbolicFrame
         from hysop.symbolic.field import curl, grad, div
-        enable_pretty_printing()
+
+        assert len(lboundaries)==4
+        assert len(rboundaries)==4
         
-        #at this moment we just test a periodic solver
         frame = SymbolicFrame(dim=3)
         coords = frame.coords
-        def gen_fn():
-            kis = tuple(random.randint(1,5) for _ in xrange(4))
-            qis = tuple(npw.random.rand(3).round(decimals=4).tolist())
-            basis = tuple( (sm.cos(ki*xi+qi), sm.sin(ki*xi+qi)) \
-                            for (ki,qi,xi) in zip(kis, qis, frame.coords) )
-            fn = sm.Integer(1)
-            for i in xrange(3):
-                fn *= random.choice(basis[i])
-            return fn
-
-        def integrate_over_domain(*fns):
-            res = ()
-            x,y,z = coords
-            for fn in fns:
-                if (fn == 0):
-                    I = 0
-                else:
-                    I = sm.integrate(fn, (x, 0, 2*sm.pi), (y, 0, 2*sm.pi), (z, 0, 2*sm.pi))
-                res += (I,)
-            return res
         
+        def gen_fn(nb_components, left_boundaries, right_boundaries):
+            assert len(left_boundaries)==len(right_boundaries)==nb_components
+            fns = ()
+            for i in xrange(nb_components):
+                if polynomial:
+                    fn, y = make_multivariate_polynomial(origin, end,
+                                                        left_boundaries[i], right_boundaries[i], 
+                                                        10, 2)
+                else:
+                    fn, y = make_multivariate_trigonometric_polynomial(origin, end,
+                            left_boundaries[i], right_boundaries[i], 2)
+                fn = fn.xreplace({yi: xi for (yi,xi) in zip(y, coords)})
+                fns += (fn,)
+            return npw.asarray(fns).view(TensorBase)
+
         analytic_solutions = {}
         analytic_functions = {}
         
-        phis  = gen_fn()
-        psis = npw.asarray([gen_fn() for _ in xrange(3)], 
-                                dtype=object).view(TensorBase)
+        psis = gen_fn(nb_components=3, 
+                      left_boundaries=lboundaries[:3],
+                      right_boundaries=rboundaries[:3])
+        phis = gen_fn(nb_components=1, 
+                      left_boundaries=lboundaries[3:],
+                      right_boundaries=rboundaries[3:])
 
         U0s = curl(psis, frame)
-        U1s = grad(phis, frame)
+        U1s = grad(phis[0], frame)
         Us = U0s + U1s
-        divUs = (div(Us, frame).tolist(),)
-        
-        divU, = integrate_over_domain(div(Us, frame)**2)
-        divU0, = integrate_over_domain(div(U0s, frame)**2)
-        divU1, = integrate_over_domain(div(U1s, frame)**2)
+
+        divU0s = to_tuple(div(U0s, frame))
+        divU1s = to_tuple(div(U1s, frame))
+        divUs  = to_tuple(div(Us, frame))
 
         fU0s   = tuple(sm.lambdify(coords, Ui) for Ui in U0s)
         fU1s   = tuple(sm.lambdify(coords, Ui) for Ui in U1s)
         fUs    = tuple(sm.lambdify(coords, Ui) for Ui in Us)
         fdivUs = tuple(sm.lambdify(coords, Ui) for Ui in divUs)
 
-        sols = {'U0s': U0s, 'U1s': U1s, 'Us':Us, 'divUs':divUs}
+        sols = {'U0s': U0s, 'U1s': U1s, 'Us':Us, 
+                'divUs':divUs, 'divU0s':divU0s, 'divU1s':divU1s}
         lambdified_sols = {'U0s': fU0s, 'U1s': fU1s, 'Us':fUs, 'divUs':fdivUs}
 
-        analytic_solutions[3] = sols
-        analytic_functions[3] = lambdified_sols
+        analytic_solutions = sols
+        analytic_functions = lambdified_sols
 
-        self.analytic_solutions = analytic_solutions
-        self.analytic_functions = analytic_functions
-        self.divU = npw.sqrt(float(divU))
-        self.divU0 = npw.sqrt(float(divU0))
-        self.divU1 = npw.sqrt(float(divU1))
+        return (analytic_solutions, analytic_functions)
 
     @classmethod
     def teardown_class(cls):
         pass
 
     
-    def _test(self, dtype,
-            size_min=None, size_max=None):
+    def _test(self, dtype, max_runs=5,
+            polynomial=False, size_min=None, size_max=None):
         enable_extra_tests = self.enable_extra_tests
 
         size_min = first_not_None(size_min, self.size_min)
         size_max = first_not_None(size_max, self.size_max)
-
+        
+        dim = 3
         valid_factors = {2,3,5,7,11,13}
         factors = {1}
         while (factors-valid_factors):
             factors.clear()
-            shape = tuple(npw.random.randint(low=size_min, high=size_max+1, size=3).tolist())
+            shape = tuple(npw.random.randint(low=size_min, high=size_max+1, size=dim).tolist())
             for Si in shape:
-                factors.update( set(primefac.primefac(int(Si-1))) )
+                factors.update( set(primefac.primefac(int(Si))) )
+        
+        domain_boundaries = list(domain_boundary_iterator(dim=dim))
+        periodic = domain_boundaries[0]
+        domain_boundaries = domain_boundaries[1:]
+        random.shuffle(domain_boundaries)
+        domain_boundaries.insert(0, periodic)
         
-        domain = Box(length=(2*npw.pi,)*3)
-        U = Field(domain=domain, name='U', dtype=dtype, 
-                nb_components=3, register_object=False)
-        U0 = Field(domain=domain, name='U0', dtype=dtype, 
-                nb_components=3, register_object=False)
-        divU  = Field(domain=domain, name='divU', dtype=dtype, 
-                nb_components=1, register_object=False)
-        divU0 = Field(domain=domain, name='divU0', dtype=dtype, 
-                nb_components=1, register_object=False)
+        for i, (lboundaries, rboundaries) in enumerate(domain_boundaries, 1):
+            domain = Box(origin=(npw.random.rand(dim)-0.5),
+                         length=(0.5+npw.random.rand(dim))*2*npw.pi,
+                         lboundaries=lboundaries,
+                         rboundaries=rboundaries)
         
-        self._test_one(shape=shape, dtype=dtype, 
-                domain=domain, U=U, U0=U0, divU=divU, divU0=divU0)
-        print
+            U     = VelocityField(domain=domain, name='U', dtype=dtype)
+            U0    = VelocityField(domain=domain, name='U0', dtype=dtype)
+            U1    = VelocityField(domain=domain, name='U1', dtype=dtype)
+            divU  = U.div(name='divU', register_object=False)
+            divU0 = U0.div(name='divU0', register_object=False)
+            divU1 = U1.div(name='divU1', register_object=False)
+
+            self._test_one(shape=shape, dtype=dtype, polynomial=polynomial,
+                    domain=domain, U=U, U0=U0, U1=U1,
+                    divU=divU, divU0=divU0, divU1=divU1)
+            if (max_runs is not None) and (i==max_runs):
+                missing = ((4**(dim+1) - 1) / 3) - i
+                print
+                print '>> MAX RUNS ACHIEVED FOR {}D DOMAINS -- SKIPING {} OTHER BOUNDARY CONDITIONS <<'.format(dim, missing)
+                print
+                print
+                break
+        else:
+            assert (i==(4**(dim+1)-1)/3), (i+1, (4**(dim+1)-1)/3)
+            print
+            print '>> TESTED ALL {}D BOUNDARY CONDITIONS <<'.format(dim)
+            print
+            print
 
     @classmethod
     def __analytic_init(cls, data, coords, dtype, fns):
@@ -153,27 +172,53 @@ class TestSolenoidalProjectionOperator(object):
             msg = 'Unknown dtype {}.'.format(dtype)
             raise NotImplementedError(msg)
 
-    def _test_one(self, shape, dtype, 
-            domain, U, U0, divU, divU0):
+    def _test_one(self, shape, dtype, polynomial,
+            domain, U, U0, U1, divU, divU0, divU1):
+        
+        print '\nTesting {}D SolenoidalProjection: dtype={} shape={}, polynomial={}, bc=[{}]'.format(
+                3, dtype.__name__, shape, polynomial, domain.format_boundaries())
+        print ' >Building U = U0 + U1 = curl(Psi) + grad(Phi)...'
+            
+        # U = curl(Psi) + grad(Phi)
+        Psi = VorticityField(velocity=U, name='Psi')
+        assert all(all(psic.lboundaries == u.lboundaries) for (psic,u) in zip(Psi.curl().fields, U.fields))
+        assert all(all(psic.rboundaries == u.rboundaries) for (psic,u) in zip(Psi.curl().fields, U.fields))
+        
+        Phi = U.div(name='Phi')
+        assert all(all(phig.lboundaries == u.lboundaries) for (phig,u) in zip(Phi.gradient().fields, U.fields))
+        assert all(all(phig.rboundaries == u.rboundaries) for (phig,u) in zip(Phi.gradient().fields, U.fields))
+            
+        lboundaries = tuple(psi.lboundaries[::-1] for psi in Psi.fields) + (Phi.lboundaries[::-1],)
+        rboundaries = tuple(psi.rboundaries[::-1] for psi in Psi.fields) + (Phi.rboundaries[::-1],)
+
+        (analytic_solutions, analytic_functions) = \
+            self.build_analytic_solutions(
+                polynomial=polynomial, 
+                lboundaries=lboundaries, # => boundaries in variable order x0,...,xn
+                rboundaries=rboundaries,
+                origin=domain.origin[::-1],
+                end=domain.end[::-1])
+        
+        del Psi
+        del Phi
+        del lboundaries
+        del rboundaries
         
-        self.build_analytic_solutions()
+        def format_expr(e):
+            return truncate_expr(round_expr(e, 3), 80)
 
-        print '\nTesting periodic {}D SolenoidalProjection: dtype={} shape={}'.format(
-                3, dtype.__name__, shape)
-        print '   Building U = U0 + U1 = rot(Psi) + grad(Phi)'
         print ' >Input analytic velocity:'
-        for (i,wi) in enumerate(self.analytic_solutions[3]['Us']):
-            print '  *U_{} = {}'.format(i, wi)
-        print '  *div(U) = {}'.format(self.analytic_solutions[3]['divUs'][0])
-        print '  L2 norm of div(U) is {}'.format(self.divU)
+        for (Ui,ui) in zip(U, analytic_solutions['Us']):
+            print '   *{} = {}'.format(Ui.pretty_name, format_expr(ui))
+        print '   *div(U) = {}'.format(format_expr(analytic_solutions['divUs'][0]))
         print ' >Expected velocity vector potential (solenoidal projection) is:'
-        for (i,ui) in enumerate(self.analytic_solutions[3]['U0s']):
-            print '  *U0_{} = {}'.format(i, ui)
-        print '  L2 norm of div(U0) is {}'.format(self.divU0)
+        for (Ui,ui) in zip(U0, analytic_solutions['U0s']):
+            print '   *{} = {}'.format(Ui.pretty_name, format_expr(ui))
+        print '   *div(U0) = {}'.format(format_expr(analytic_solutions['divU0s'][0]))
         print ' >Expected velocity scalar potential is:'
-        for (i,ui) in enumerate(self.analytic_solutions[3]['U1s']):
-            print '  *U1_{} = {}'.format(i, ui)
-        print '  L2 norm of div(U1) is {}'.format(self.divU1)
+        for (Ui,ui) in zip(U1, analytic_solutions['U1s']):
+            print '   *{} = {}'.format(Ui.pretty_name, format_expr(ui))
+        print '   *div(U1) = {}'.format(format_expr(analytic_solutions['divU1s'][0]))
         print ' >Testing all available implementations:'
 
         implementations = SolenoidalProjection.implementations()
@@ -198,6 +243,15 @@ class TestSolenoidalProjectionOperator(object):
                                                           cl_env.device.name.strip())
                     print msg,
                     yield SolenoidalProjection(cl_env=cl_env, **base_kwds)
+                msg='   *OpenCl FFTW: '
+                print msg
+                cpu_envs = tuple(iter_clenv(device_type='cpu'))
+                if cpu_envs:
+                    for cl_env in cpu_envs:
+                        msg='     |platform {}, device {}'.format(cl_env.platform.name.strip(),
+                                                                  cl_env.device.name.strip())
+                        print msg,
+                        yield SolenoidalProjection(cl_env=cl_env, enforce_implementation=False, **base_kwds)
             else:
                 msg='Unknown implementation to test {}.'.format(impl)
                 raise NotImplementedError(msg)
@@ -207,23 +261,26 @@ class TestSolenoidalProjectionOperator(object):
         for impl in implementations:
             for op in iter_impl(impl):
                 op  = op.build()
+
                 du      = op.get_input_discrete_field(U)
                 du0     = op.get_output_discrete_field(U0)
                 du_div  = op.get_output_discrete_field(divU)
                 du0_div = op.get_output_discrete_field(divU0)
                 
                 du.initialize(self.__analytic_init, dtype=dtype, 
-                                    fns=self.analytic_functions[3]['Us'])
+                                    fns=analytic_functions['Us'])
+
                 if (Uref is None):
                     du0.initialize(self.__analytic_init, dtype=dtype, 
-                                    fns=self.analytic_functions[3]['U0s'])
+                                    fns=analytic_functions['U0s'])
                     du_div.initialize(self.__analytic_init, dtype=dtype, 
-                                    fns=self.analytic_functions[3]['divUs'])
+                                    fns=analytic_functions['divUs'])
                     du0_div.initialize(self.__zero_init, dtype=dtype)
                     Uref     = tuple( data.get().handle.copy() for data in du.data )
                     U0ref    = tuple( data.get().handle.copy() for data in du0.data )
                     divUref  = tuple( data.get().handle.copy() for data in du_div.data )
                     divU0ref = tuple( data.get().handle.copy() for data in du0_div.data )
+
                 du0.initialize(self.__random_init, dtype=dtype)
                 du_div.initialize(self.__random_init, dtype=dtype)
                 du0_div.initialize(self.__random_init, dtype=dtype)
@@ -236,7 +293,7 @@ class TestSolenoidalProjectionOperator(object):
                 divU0out = tuple( data.get().handle.copy() for data in du0_div.data )
 
                 s = npw.prod(du.space_step)
-                print 'divU={}, divU0={}'.format(
+                print '[divU={}, divU0={}]'.format(
                         npw.sqrt(npw.sum(divUout[0]**2)*s),
                         npw.sqrt(npw.sum(divU0out[0]**2)*s)),
 
@@ -248,10 +305,10 @@ class TestSolenoidalProjectionOperator(object):
     def _check_output(cls, impl, op, 
                       Uref, divUref, U0ref, divU0ref, 
                       Uout, divUout, U0out, divU0out):
-        check_instance(Uref,  tuple, values=npw.ndarray, size=3)
-        check_instance(U0ref, tuple, values=npw.ndarray, size=3)
-        check_instance(Uout,  tuple, values=npw.ndarray, size=3)
-        check_instance(U0out, tuple, values=npw.ndarray, size=3)
+        check_instance(Uref,     tuple, values=npw.ndarray, size=3)
+        check_instance(U0ref,    tuple, values=npw.ndarray, size=3)
+        check_instance(Uout,     tuple, values=npw.ndarray, size=3)
+        check_instance(U0out,    tuple, values=npw.ndarray, size=3)
         check_instance(divUref,  tuple, values=npw.ndarray, size=1)
         check_instance(divU0ref, tuple, values=npw.ndarray, size=1)
         check_instance(divUout,  tuple, values=npw.ndarray, size=1)
@@ -272,9 +329,9 @@ class TestSolenoidalProjectionOperator(object):
                     msg = msg0.format(iname)
                     raise ValueError(msg)
         
-        for (out_buffers, ref_buffers, name) in zip((Uout, U0out, divUout, U0out, divU0out), 
-                                                    (Uref, U0ref, divUref, U0ref, divU0ref), 
-                                                    ('U', 'U0', 'divU', 'U0', 'divU0')):
+        for (out_buffers, ref_buffers, name) in zip((Uout, U0out, divUout, divU0out), 
+                                                    (Uref, U0ref, divUref, divU0ref), 
+                                                    ('U', 'U0', 'divU', 'divU0')):
             print '| {}=('.format(name),
             for i, (fout,fref) in enumerate(zip(out_buffers, ref_buffers)):
                 iname = '{}{}'.format(name,i)
@@ -288,11 +345,12 @@ class TestSolenoidalProjectionOperator(object):
                     deps = int(npw.ceil(dinf/eps))
                 except ValueError:
                     deps = npw.nan
-                if (deps < 20000):
+                if (deps < 10000):
                     print '{}eps'.format(deps),
                     if (i!=len(out_buffers)-1):
                         print ',',
                     continue
+
                 has_nan = npw.any(npw.isnan(fout))
                 has_inf = npw.any(npw.isinf(fout))
 
@@ -334,14 +392,17 @@ class TestSolenoidalProjectionOperator(object):
             print ')',
 
 
-    def test_3d_float32(self):
-        self._test(dtype=npw.float32)
-    def test_3d_float64(self):
-        self._test(dtype=npw.float64)
+    def test_3d_float32(self, **kwds):
+        self._test(dtype=npw.float32, **kwds)
+    def test_3d_float64(self, **kwds):
+        self._test(dtype=npw.float64, **kwds)
 
     def perform_tests(self):
-        self.test_3d_float32()
-        self.test_3d_float64()
+        max_3d_runs = None if __ENABLE_LONG_TESTS__ else 3
+        if __ENABLE_LONG_TESTS__ or (HYSOP_REAL==npw.float32):
+            self.test_3d_float32(max_runs=max_3d_runs)
+        if __ENABLE_LONG_TESTS__ or (HYSOP_REAL==npw.float64):
+            self.test_3d_float64(max_runs=max_3d_runs)
     
 if __name__ == '__main__':
     TestSolenoidalProjectionOperator.setup_class(enable_extra_tests=False, 
diff --git a/hysop/operator/tests/test_spectral_curl.py b/hysop/operator/tests/test_spectral_curl.py
new file mode 100644
index 0000000000000000000000000000000000000000..3407397bd8ab9496da1f1fe72c32c75055b776dc
--- /dev/null
+++ b/hysop/operator/tests/test_spectral_curl.py
@@ -0,0 +1,354 @@
+import random, primefac
+from hysop.deps import it, sm, random
+from hysop.constants import HYSOP_REAL, BoxBoundaryCondition
+from hysop.defaults import VelocityField, VorticityField
+from hysop.testsenv import __ENABLE_LONG_TESTS__, __HAS_OPENCL_BACKEND__
+from hysop.testsenv import opencl_failed, iter_clenv, test_context, domain_boundary_iterator
+from hysop.tools.contexts import printoptions
+from hysop.tools.numerics import is_fp, is_integer
+from hysop.tools.types import check_instance, first_not_None
+from hysop.tools.io_utils import IO
+from hysop.tools.numpywrappers import npw
+from hysop.tools.sympy_utils import truncate_expr, round_expr
+from hysop.tools.spectral_utils import make_multivariate_trigonometric_polynomial, \
+                                       make_multivariate_polynomial
+from hysop.operator.curl import SpectralCurl, Implementation
+from hysop.defaults import VorticityField, VelocityField
+
+from hysop import Field, Box
+
+class TestSpectralCurl(object):
+
+    @classmethod
+    def setup_class(cls,
+            enable_extra_tests=__ENABLE_LONG_TESTS__,
+            enable_debug_mode=False):
+
+        IO.set_default_path('/tmp/hysop_tests/test_spectral_curl')
+
+        cls.size_min = 8
+        cls.size_max = 16
+
+        cls.enable_extra_tests = enable_extra_tests
+        cls.enable_debug_mode  = enable_debug_mode
+        
+        from hysop.tools.sympy_utils import enable_pretty_printing
+        enable_pretty_printing()
+    
+    @classmethod
+    def teardown_class(cls):
+        pass
+
+
+    @classmethod
+    def build_analytic_solutions(cls, polynomial, 
+                                      dim, nb_components,
+                                      lboundaries, rboundaries,
+                                      origin, end):
+        from hysop.symbolic.base  import TensorBase
+        from hysop.symbolic.frame import SymbolicFrame
+        from hysop.symbolic.field import laplacian, curl
+
+        assert len(lboundaries)==nb_components
+        assert len(rboundaries)==nb_components
+        
+        frame  = SymbolicFrame(dim=dim)
+        coords = frame.coords
+                
+        def gen_Fin():
+            Fins = ()
+            for i in xrange(nb_components):
+                if polynomial:
+                    fin, y = make_multivariate_polynomial(origin, end,
+                                                        lboundaries[i], rboundaries[i], 
+                                                        10, 4)
+                else:
+                    fin, y = make_multivariate_trigonometric_polynomial(origin, end,
+                            lboundaries[i], rboundaries[i], 2)
+                fin = fin.xreplace({yi: xi for (yi,xi) in zip(y, coords)})
+                Fins += (fin,)
+            return npw.asarray(Fins).view(TensorBase)
+        
+        Fins  = gen_Fin()
+        Fouts = npw.atleast_1d(curl(Fins, frame))
+
+        fFins  = tuple(sm.lambdify(coords, Fin) for Fin in Fins)
+        fFouts = tuple(sm.lambdify(coords, Fout) for Fout in Fouts)
+
+        analytic_expressions = {'Fin':Fins,  'Fout':Fouts}
+        analytic_functions   = {'Fin':fFins, 'Fout':fFouts}
+        return (analytic_expressions, analytic_functions)
+
+    
+    @staticmethod
+    def __random_init(data, coords, dtype):
+        for d in data:
+            if is_fp(d.dtype):
+                d[...] = npw.random.random(size=d.shape).astype(dtype=d.dtype)
+            else:
+                msg = 'Unknown dtype {}.'.format(d.dtype)
+                raise NotImplementedError(msg)
+    
+    @staticmethod
+    def __analytic_init(data, coords, dtype, fns):
+        assert len(fns) == len(data)
+        for (d,fn,coord) in zip(data,fns,coords):
+            coord = tuple(c.astype(d.dtype) for c in coord)
+            d[...] = fn(*coord).astype(d.dtype)
+
+
+
+    def _test(self, dim, dtype, nb_components, max_runs=5,
+            polynomial=False, size_min=None, size_max=None):
+        enable_extra_tests = self.enable_extra_tests
+
+        size_min = first_not_None(size_min, self.size_min)
+        size_max = first_not_None(size_max, self.size_max)
+
+        valid_factors = {2,3,5,7,11,13}
+        factors = {1}
+        while (factors-valid_factors):
+            factors.clear()
+            shape = tuple(npw.random.randint(low=size_min, high=size_max+1, size=dim).tolist())
+            for Si in shape:
+                factors.update( set(primefac.primefac(int(Si))) )
+        
+        domain_boundaries = list(domain_boundary_iterator(dim=dim))
+        periodic = domain_boundaries[0]
+        domain_boundaries = domain_boundaries[1:]
+        random.shuffle(domain_boundaries)
+        domain_boundaries.insert(0, periodic)
+
+        for i, (lboundaries, rboundaries) in enumerate(domain_boundaries, 1):
+            
+            domain = Box(origin=(npw.random.rand(dim)-0.5),
+                         length=(0.5+npw.random.rand(dim)*2*npw.pi),
+                            lboundaries=lboundaries,
+                            rboundaries=rboundaries)
+            
+            if (dim==nb_components):
+                Fin  = VelocityField(name='Fin', domain=domain)
+                Fout = VorticityField(name='Fout', velocity=Fin)
+            else:
+                Fin  = Field(name='Fin', domain=domain, dtype=dtype, nb_components=nb_components)
+                Fout = Fin.curl(name='Fout')
+
+            self._test_one(shape=shape, dim=dim, dtype=dtype, 
+                    domain=domain, Fin=Fin, Fout=Fout, polynomial=polynomial)
+            if (max_runs is not None) and (i==max_runs):
+                missing = ((4**(dim+1) - 1) / 3) - i
+                print
+                print '>> MAX RUNS ACHIEVED FOR {}D DOMAINS -- SKIPING {} OTHER BOUNDARY CONDITIONS <<'.format(dim, missing)
+                print
+                print
+                break
+        else:
+            assert (i==(4**(dim+1)-1)/3), (i+1, (4**(dim+1)-1)/3)
+            print
+            print '>> TESTED ALL {}D BOUNDARY CONDITIONS <<'.format(dim)
+            print
+            print
+
+    def _test_one(self, shape, dim, dtype,
+            domain, Fout, Fin, polynomial):
+        
+        (analytic_expressions, analytic_functions) = \
+            self.build_analytic_solutions(
+                dim=dim, nb_components=Fin.nb_components, polynomial=polynomial, 
+                lboundaries=[fin.lboundaries[::-1] for fin in Fin.fields], # => boundaries in variable order x0,...,xn
+                rboundaries=[fin.rboundaries[::-1] for fin in Fin.fields],
+                origin=domain.origin[::-1],
+                end=domain.end[::-1])
+        
+        def format_expr(e):
+            return truncate_expr(round_expr(e, 3), 80)
+
+        msg='\nTesting {}D Curl: dtype={} shape={} polynomial={}, bc=[{}]'.format(
+                dim, dtype.__name__, shape, polynomial, domain.format_boundaries())
+        print msg
+        print ' >Input analytic field is (truncated):'
+        for (fin, fins) in zip(Fin.fields, analytic_expressions['Fin']):
+            print '  *{}(x) = {}'.format(fin.pretty_name, format_expr(fins))
+        print ' >Expected output analytic field is:'
+        for (fout, fouts) in zip(Fout.fields, analytic_expressions['Fout']):
+            print '  *{}(x) = {}'.format(fout.pretty_name, format_expr(fouts))
+        print ' >Testing all implementations:'
+
+        implementations = SpectralCurl.implementations().keys()
+        variables = { Fout:shape, Fin:shape }
+
+        def iter_impl(impl):
+            base_kwds = dict(Fin=Fin, Fout=Fout, variables=variables,
+                             implementation=impl,
+                             name='curl_{}'.format(str(impl).lower()))
+            if impl is Implementation.PYTHON:
+                msg='   *Python FFTW: '
+                print msg,
+                yield SpectralCurl(**base_kwds)
+            elif impl is Implementation.OPENCL:
+                msg='   *OpenCl CLFFT: '
+                print msg
+                for cl_env in iter_clenv():
+                    msg='     |platform {}, device {}'.format(cl_env.platform.name.strip(),
+                                                              cl_env.device.name.strip())
+                    print msg,
+                    yield SpectralCurl(cl_env=cl_env, **base_kwds)
+            else:
+                msg='Unknown implementation to test {}.'.format(impl)
+                raise NotImplementedError(msg)
+
+        # Compare to analytic solution
+        Fout_ref = None
+        Fin_ref = None
+        for impl in implementations:
+            for (i,op) in enumerate(iter_impl(impl)):
+                from hysop.tools.debug_dumper import DebugDumper
+                name='{}_{}'.format(impl, i)
+
+                op = op.build()
+                
+                dFin = op.get_input_discrete_field(Fin).as_contiguous_dfield()
+                dFout = op.get_output_discrete_field(Fout).as_contiguous_dfield()
+                
+                dFin.initialize(self.__analytic_init, dtype=dtype,
+                                    fns=analytic_functions['Fin'])
+
+                if (Fout_ref is None):
+                    dFout.initialize(self.__analytic_init, dtype=dtype,
+                                    fns=analytic_functions['Fout'])
+                    Fin_ref = tuple( data.get().handle.copy() for data in dFin.data )
+                    Fout_ref = tuple( data.get().handle.copy() for data in dFout.data )
+                dFout.initialize(self.__random_init, dtype=dtype)
+
+                op.apply(simulation=None)
+
+                Wout = tuple( data.get().handle.copy() for data in dFin.data )
+                Uout = tuple( data.get().handle.copy() for data in dFout.data )
+                self._check_output(impl, op, Fin_ref, Fout_ref, Wout, Uout)
+                print
+
+    @classmethod
+    def _check_output(cls, impl, op, Fin_ref, Fout_ref, Wout, Uout):
+        check_instance(Fin_ref, tuple, values=npw.ndarray)
+        check_instance(Fout_ref, tuple, values=npw.ndarray)
+        check_instance(Wout, tuple, values=npw.ndarray, size=len(Fin_ref))
+        check_instance(Uout, tuple, values=npw.ndarray, size=len(Fout_ref))
+
+        msg0 = 'Reference field {} is not finite.'
+        for (fields, name) in zip((Fin_ref, Fout_ref),('Fin_ref', 'Fout_ref')):
+            for (i,field) in enumerate(fields):
+                iname = '{}{}'.format(name,i)
+                mask = npw.isfinite(field)
+                if not mask.all():
+                    print
+                    print field
+                    print
+                    print field[~mask]
+                    print
+                    msg = msg0.format(iname)
+                    raise ValueError(msg)
+
+        for (out_buffers, ref_buffers, name) in zip((Wout, Uout), (Fin_ref, Fout_ref), ('Fin', 'Fout')):
+            for i, (fout,fref) in enumerate(zip(out_buffers, ref_buffers)):
+                iname = '{}{}'.format(name,i)
+                assert fout.dtype == fref.dtype, iname
+                assert fout.shape == fref.shape, iname
+                assert not npw.any(npw.isnan(fref))
+                assert not npw.any(npw.isinf(fref))
+
+                has_nan = npw.any(npw.isnan(fout))
+                has_inf = npw.any(npw.isinf(fout))
+                if has_nan:
+                    deps = 'nan'
+                elif has_inf:
+                    deps = 'inf'
+                else:
+                    eps  = npw.finfo(fout.dtype).eps
+                    dist = npw.abs(fout-fref)
+                    dinf = npw.max(dist)
+                    try:
+                        deps = int(npw.ceil(dinf/eps))
+                    except:
+                        deps = 'inf'
+                if (deps < 10000) or True:
+                    print '{}eps, '.format(deps),
+                    continue
+
+                print
+                print
+                print 'Test output comparisson for {} failed for component {}:'.format(name, i)
+                print ' *has_nan: {}'.format(has_nan)
+                print ' *has_inf: {}'.format(has_inf)
+                print ' *dinf={} ({} eps)'.format(dinf, deps)
+                print
+                if cls.enable_debug_mode:
+                    print 'REFERENCE INPUTS:'
+                    for (i,w) in enumerate(Fin_ref):
+                        print 'Fin{}'.format(i)
+                        print w
+                        print
+                    if (name == 'Fout'):
+                        print 'REFERENCE OUTPUT:'
+                        for (i,u) in enumerate(Fout_ref):
+                            print 'Fout{}'.format(i)
+                            print u
+                            print
+                        print
+                        print 'OPERATOR {} OUTPUT:'.format(op.name.upper())
+                        print
+                        for (i,u) in enumerate(Uout):
+                            print 'Fout{}'.format(i)
+                            print u
+                            print
+                    else:
+                        print 'MODIFIED INPUTS:'
+                        for (i,w) in enumerate(Wout):
+                            print 'Fin{}'.format(i)
+                            print w
+                            print
+                    print
+
+                msg = 'Test failed for {} on component {} for implementation {}.'
+                msg = msg.format(name, i, impl)
+                raise RuntimeError(msg)
+
+
+    def test_2d_float32__1(self, **kwds):
+        self._test(dim=2, dtype=npw.float32, nb_components=1, **kwds)
+    def test_2d_float32__2(self, **kwds):
+        self._test(dim=2, dtype=npw.float32, nb_components=2, **kwds)
+    def test_3d_float32(self, **kwds):
+        self._test(dim=3, dtype=npw.float32, nb_components=3, **kwds)
+    
+    def test_2d_float64__1(self, **kwds):
+        self._test(dim=2, dtype=npw.float64, nb_components=1, **kwds)
+    def test_2d_float64__2(self, **kwds):
+        self._test(dim=2, dtype=npw.float64, nb_components=2, **kwds)
+    def test_3d_float64(self, **kwds):
+        self._test(dim=3, dtype=npw.float64, nb_components=3, **kwds)
+
+    def perform_tests(self):
+        max_2d_runs = None if __ENABLE_LONG_TESTS__ else 2
+        max_3d_runs = None if __ENABLE_LONG_TESTS__ else 2
+
+        if __ENABLE_LONG_TESTS__ or (HYSOP_REAL==npw.float32):
+            self.test_2d_float32__1(max_runs=max_2d_runs)
+            self.test_2d_float32__2(max_runs=max_2d_runs)
+            self.test_3d_float32(max_runs=max_3d_runs)
+        if __ENABLE_LONG_TESTS__ or (HYSOP_REAL==npw.float64):
+            self.test_2d_float64__1(max_runs=max_2d_runs)
+            self.test_2d_float64__2(max_runs=max_2d_runs)
+            self.test_3d_float64(max_runs=max_3d_runs)
+
+if __name__ == '__main__':
+    TestSpectralCurl.setup_class(enable_extra_tests=False,
+                                 enable_debug_mode=False)
+
+    test = TestSpectralCurl()
+
+    with printoptions(threshold=10000, linewidth=240,
+                      nanstr='nan', infstr='inf',
+                      formatter={'float': lambda x: '{:>6.2f}'.format(x)}):
+        test.perform_tests()
+
+    TestSpectralCurl.teardown_class()
diff --git a/hysop/operator/tests/test_spectral_derivative.py b/hysop/operator/tests/test_spectral_derivative.py
new file mode 100644
index 0000000000000000000000000000000000000000..5702c7f73ec2256b14f911062cc23b97f4a072aa
--- /dev/null
+++ b/hysop/operator/tests/test_spectral_derivative.py
@@ -0,0 +1,373 @@
+"""
+Test gradient of fields.
+"""
+from hysop.deps import it, sm, random
+from hysop.constants import HYSOP_REAL, Backend, BoundaryCondition, BoxBoundaryCondition
+from hysop.methods import SpaceDiscretization
+from hysop.testsenv import __ENABLE_LONG_TESTS__, __HAS_OPENCL_BACKEND__
+from hysop.testsenv import opencl_failed, iter_clenv, domain_boundary_iterator
+from hysop.tools.contexts import printoptions
+from hysop.tools.numerics import is_fp, is_integer
+from hysop.tools.types import check_instance, first_not_None
+from hysop.tools.io_utils import IO
+from hysop.tools.numpywrappers import npw
+from hysop.tools.sympy_utils import truncate_expr, round_expr
+from hysop.tools.spectral_utils import make_multivariate_trigonometric_polynomial, \
+                                       make_multivariate_polynomial
+from hysop.parameters.scalar_parameter import ScalarParameter
+from hysop.operator.derivative import Implementation, SpectralSpaceDerivative
+from hysop.operator.gradient import Gradient
+from hysop.operator.misc import ForceTopologyState
+
+from hysop import Field, Box
+
+class TestSpectralDerivative(object):
+
+    @classmethod
+    def setup_class(cls, 
+            enable_extra_tests=__ENABLE_LONG_TESTS__,
+            enable_debug_mode=False):
+
+        IO.set_default_path('/tmp/hysop_tests/test_spectral_derivative')
+        
+        cls.size_min = 8
+        cls.size_max = 16
+        
+        cls.enable_extra_tests = enable_extra_tests
+        cls.enable_debug_mode  = enable_debug_mode
+        
+        cls.t = ScalarParameter(name='t', dtype=HYSOP_REAL)
+    
+    @classmethod
+    def build_analytic_expressions(cls, polynomial, dim, max_derivative,
+                                        lboundaries, rboundaries, origin, end):
+        from hysop.tools.sympy_utils import enable_pretty_printing
+        from hysop.symbolic.base import TensorBase
+        from hysop.symbolic.frame import SymbolicFrame
+        from hysop.symbolic.field import curl, laplacian
+        enable_pretty_printing()
+        
+        frame = SymbolicFrame(dim=dim)
+        coords = frame.coords
+        params = coords + (cls.t.s,)
+        
+        def gen_F():
+            if polynomial:
+                f, y = make_multivariate_polynomial(origin, end,
+                                                    lboundaries, rboundaries, 
+                                                    10, 4)
+            else:
+                f, y = make_multivariate_trigonometric_polynomial(origin, end,
+                        lboundaries, rboundaries, 2)
+            f = f.xreplace({yi: xi for (yi,xi) in zip(y, frame.coords)})
+            f *= sm.Integer(1) / (sm.Integer(1) + npw.random.randint(1,5)*cls.t.s)
+            return f
+
+        F  = gen_F()
+        fF = sm.lambdify(params, F)
+    
+        dFs  = {}
+        fdFs = {}
+        symbolic_dvars = {}
+        for idx in it.product(range(max_derivative+1), repeat=dim):
+            if sum(idx)> max_derivative:
+                continue
+            xvars = tuple((ci,i) for (i,ci) in zip(idx, coords))
+            symbolic_dvars[idx] = xvars
+            dF = F
+            for (ci,i) in xvars:
+                if (i==0):
+                    continue
+                dF = dF.diff(ci,i)
+            dFs[idx]  = dF
+            fdFs[idx] = sm.lambdify(params, dF) 
+
+        analytic_expressions = {'F':F,  'dF':dFs}
+        analytic_functions = {'F':fF, 'dF':fdFs}
+        return (symbolic_dvars, analytic_expressions, analytic_functions)
+
+
+    @classmethod
+    def teardown_class(cls):
+        pass
+
+    @staticmethod
+    def __random_init(data, coords):
+        for d in data:
+            if is_fp(d.dtype):
+                    d[...] = npw.random.random(size=d.shape).astype(dtype=d.dtype)
+            else:
+                msg = 'Unknown dtype {}.'.format(d.dtype)
+                raise NotImplementedError(msg)
+
+    @staticmethod
+    def __analytic_init(data, coords, fns, t):
+        assert len(fns) == len(data)
+        for (d,fn,coord) in zip(data,fns,coords):
+            coord = tuple(c.astype(d.dtype) for c in coord)
+            d[...] = fn(*(coord+(t(),))).astype(d.dtype)
+
+    def _test(self, dim, dtype, polynomial, max_derivative=2,
+            size_min=None, size_max=None, max_runs=None):
+        enable_extra_tests = self.enable_extra_tests
+        
+        size_min = first_not_None(size_min, self.size_min)
+        size_max = first_not_None(size_max, self.size_max)
+
+        shape = tuple(npw.random.randint(low=size_min, high=size_max+1, size=dim).tolist())
+        
+        domain_boundaries = list(domain_boundary_iterator(dim=dim))
+        periodic = domain_boundaries[0]
+        domain_boundaries = domain_boundaries[1:]
+        random.shuffle(domain_boundaries)
+        domain_boundaries.insert(0, periodic)
+        
+        for i, (lboundaries, rboundaries) in enumerate(domain_boundaries, 1):
+            domain = Box(origin=(npw.random.rand(dim)-0.5),
+                         length=(npw.random.rand(dim)+0.5)*2*npw.pi,
+                         lboundaries=lboundaries,
+                         rboundaries=rboundaries)
+
+            F = Field(domain=domain, name='F', dtype=dtype)
+
+            self._test_one(shape=shape, dim=dim, dtype=dtype, 
+                    domain=domain, F=F, 
+                    polynomial=polynomial, 
+                    max_derivative=max_derivative)
+            
+            if (max_runs is not None) and (i==max_runs):
+                missing = ((4**(dim+1) - 1) / 3) - i
+                print
+                print '>> MAX RUNS ACHIEVED FOR {}D DOMAINS -- SKIPING {} OTHER BOUNDARY CONDITIONS <<'.format(dim, missing)
+                print
+                print
+                break
+        else:
+            assert (i==(4**(dim+1)-1)/3), (i+1, (4**(dim+1)-1)/3)
+            print
+            print '>> TESTED ALL {}D BOUNDARY CONDITIONS <<'.format(dim)
+            print
+            print
+
+    
+    def _test_one(self, shape, dim, dtype, 
+            domain, F, polynomial, max_derivative):
+        
+        implementations = SpectralSpaceDerivative.implementations()
+
+        (symbolic_dvars, analytic_expressions, analytic_functions) = \
+            self.build_analytic_expressions(
+                dim=dim, polynomial=polynomial, 
+                max_derivative=max_derivative,
+                lboundaries=F.lboundaries[::-1], # => boundaries in variable order x0,...,xn
+                rboundaries=F.rboundaries[::-1],
+                origin=domain.origin[::-1],
+                end=domain.end[::-1])
+        
+        Fs  = analytic_expressions['F']
+        fFs = analytic_functions['F']
+        
+        def format_expr(e):
+            return truncate_expr(round_expr(e, 3), 80)
+
+        msg='\nTesting {}D SpectralDerivative: dtype={} shape={}, polynomial={}, bc=[{}]'
+        msg=msg.format(dim, dtype.__name__, shape, polynomial, F.domain.format_boundaries())
+        msg+='\n >Corresponding field boundary conditions are [{}].'.format(F.format_boundaries())
+        msg+='\n >Input analytic functions (truncated):'
+        msg+='\n   *{}(x,t) = {}'.format(F.pretty_name, format_expr(Fs))
+        msg+='\n >Testing derivatives:'
+        print msg
+
+        for idx in sorted(symbolic_dvars.keys(), key=lambda x: sum(x)):
+            xvars = symbolic_dvars[idx]
+            dFe = F.s()
+            for (ci,i) in xvars:
+                if (i==0):
+                    continue
+                dFe = dFe.diff(ci,i)
+            dF = F.from_sympy_expression(expr=dFe,
+                    space_symbols=domain.frame.coords)
+            dFs   = analytic_expressions['dF'][idx]
+            fdFs  = analytic_functions['dF'][idx]
+            print '   *{}'.format(dF.pretty_name)
+            
+            variables = { F:shape, dF: shape }
+        
+            def iter_impl(impl):
+                base_kwds = dict(F=F, dF=dF, derivative=idx, 
+                                 variables=variables,
+                                 implementation=impl,
+                                 testing=True)
+                if impl is Implementation.PYTHON:
+                    msg='     |Python: '
+                    print msg,
+                    op = SpectralSpaceDerivative(**base_kwds)
+                    yield op.to_graph()
+                    print
+                elif impl is Implementation.OPENCL:
+                    msg='     |Opencl: '
+                    print msg
+                    for cl_env in iter_clenv():
+                        msg='        >platform {}, device {}:'.format(
+                                                              cl_env.platform.name.strip(), 
+                                                              cl_env.device.name.strip())
+                        print msg,
+                        op = SpectralSpaceDerivative(cl_env=cl_env, **base_kwds)
+                        yield op.to_graph()
+                        print
+                    print
+                else:
+                    msg='Unknown implementation to test {}.'.format(impl)
+                    raise NotImplementedError(msg)
+        
+            # Compare to analytic solution
+            Fref = None
+            for impl in implementations:
+                for op in iter_impl(impl):
+                    op.build(outputs_are_inputs=False)
+                    #op.display()
+
+                    Fd  = op.get_input_discrete_field(F)
+                    dFd = op.get_output_discrete_field(dF)
+
+                    if (Fref is None):
+                        dFd.initialize(self.__analytic_init, fns=(fdFs,), t=self.t)
+                        dFref = tuple( data.get().handle.copy() for data in dFd.data )
+
+                    Fd.initialize(self.__analytic_init, fns=(fFs,), t=self.t)
+                    Fref = tuple( data.get().handle.copy() for data in Fd.data )
+
+                    dFd.initialize(self.__random_init)
+                    op.apply()
+                    
+                    Fout  = tuple( data.get().handle.copy() for data in Fd.data )
+                    dFout = tuple( data.get().handle.copy() for data in dFd.data )
+                    
+                    self._check_output(impl, op, Fref, dFref, Fout, dFout, idx)
+    
+    @classmethod
+    def _check_output(cls, impl, op, Fref, dFref, Fout, dFout, idx):
+        nidx = sum(idx)
+        check_instance(Fref,  tuple, values=npw.ndarray)
+        check_instance(dFref, tuple, values=npw.ndarray)
+        check_instance(Fout,  tuple, values=npw.ndarray, size=len(Fref))
+        check_instance(dFout, tuple, values=npw.ndarray, size=len(dFref))
+
+        for j,(out_buffers, ref_buffers, name) in enumerate(zip((Fout, dFout), 
+                                                                (Fref, dFref), 
+                                                                ('F', 'dF'))):
+            for i, (fout,fref) in enumerate(zip(out_buffers, ref_buffers)):
+                iname = '{}{}'.format(name,i)
+                assert fout.dtype == fref.dtype, iname
+                assert fout.shape == fref.shape, iname
+                
+                assert not npw.any(npw.isnan(fref))
+                assert not npw.any(npw.isinf(fref))
+                has_nan = npw.any(npw.isnan(fout))
+                has_inf = npw.any(npw.isinf(fout))
+                
+                if (has_nan or has_inf):
+                    pass
+                else:
+                    eps  = npw.finfo(fout.dtype).eps
+                    dist = npw.abs(fout-fref)
+                    dinf = npw.max(dist)
+                    deps = int(dinf/eps)
+                    if (deps <= 10**(nidx+2)):
+                        if (j==1):
+                            print '{}eps ({})'.format(deps, dinf),
+                        else:
+                            print '{}eps, '.format(deps),
+                        continue
+                
+                print
+                print
+                print 'Test output comparisson for {} failed for component {}:'.format(name, i)
+                print ' *has_nan: {}'.format(has_nan)
+                print ' *has_inf: {}'.format(has_inf)
+                print ' *dinf={}'.format(dinf)
+                print ' *deps={}'.format(deps)
+                print
+                if cls.enable_debug_mode:
+                    print 'REFERENCE INPUTS:'
+                    for (i,w) in enumerate(Fref):
+                        print 'F{}'.format(i)
+                        print w
+                        print
+                    if (name == 'dF'):
+                        print 'REFERENCE OUTPUT:'
+                        for (i,u) in enumerate(dFref):
+                            print 'dF{}'.format(i)
+                            print u
+                            print
+                        print
+                        print 'OPERATOR {} OUTPUT:'.format(op.name.upper())
+                        print
+                        for (i,u) in enumerate(dFout):
+                            print 'dF{}'.format(i)
+                            print u
+                            print
+                    else:
+                        print 'MODIFIED INPUTS:'
+                        for (i,w) in enumerate(Fout):
+                            print 'F{}'.format(i)
+                            print w
+                            print
+                    print
+                
+                msg = 'Test failed for {} on component {} for implementation {}.'.format(name, 
+                        i, impl)
+                raise RuntimeError(msg) 
+
+
+
+    def test_1d_trigonometric_float32(self, **kwds):
+        self._test(dim=1, dtype=npw.float32, polynomial=False, **kwds)
+    def test_2d_trigonometric_float32(self, **kwds):
+        self._test(dim=2, dtype=npw.float32, polynomial=False, **kwds)
+    def test_3d_trigonometric_float32(self, **kwds):
+        self._test(dim=3, dtype=npw.float32, polynomial=False, **kwds)
+
+    def test_1d_trigonometric_float64(self, **kwds):
+        self._test(dim=1, dtype=npw.float64, polynomial=False, **kwds)
+    def test_2d_trigonometric_float64(self, **kwds):
+        self._test(dim=2, dtype=npw.float64, polynomial=False, **kwds)
+    def test_3d_trigonometric_float64(self, **kwds):
+        self._test(dim=3, dtype=npw.float64, polynomial=False, **kwds)
+    
+    def test_1d_polynomial_float32(self, **kwds):
+        self._test(dim=1, dtype=npw.float32, polynomial=True, **kwds)
+    def test_2d_polynomial_float32(self, **kwds):
+        self._test(dim=2, dtype=npw.float32, polynomial=True, **kwds)
+    def test_3d_polynomial_float32(self, **kwds):
+        self._test(dim=3, dtype=npw.float32, polynomial=True, **kwds)
+
+    def perform_tests(self):
+        max_2d_runs = None if __ENABLE_LONG_TESTS__ else 2
+        max_3d_runs = None if __ENABLE_LONG_TESTS__ else 2
+
+        self.test_1d_trigonometric_float32(max_derivative=3)
+        self.test_2d_trigonometric_float32(max_derivative=2, max_runs=max_2d_runs)
+        self.test_3d_trigonometric_float32(max_derivative=1, max_runs=max_3d_runs)
+
+        if __ENABLE_LONG_TESTS__:
+            self.test_1d_trigonometric_float64(max_derivative=3)
+            self.test_2d_trigonometric_float64(max_derivative=2)
+            self.test_3d_trigonometric_float64(max_derivative=1)
+
+            self.test_1d_polynomial_float32(max_derivative=3)
+            self.test_2d_polynomial_float32(max_derivative=2)
+            self.test_3d_polynomial_float32(max_derivative=1)
+    
+if __name__ == '__main__':
+    TestSpectralDerivative.setup_class(enable_extra_tests=False, 
+                                     enable_debug_mode=False)
+    
+    test = TestSpectralDerivative()
+
+    with printoptions(threshold=10000, linewidth=1000, 
+                      nanstr='nan', infstr='inf',
+                      formatter={'float': lambda x: '{:>6.2f}'.format(x)}):
+        test.perform_tests()
+
+    TestSpectralDerivative.teardown_class()
diff --git a/hysop/operator/tests/test_transpose.py b/hysop/operator/tests/test_transpose.py
index ceca71b5a940367789dbf4cea0f34451eba299a9..6ebe79df3d9fbc2c4bd47535218a6af2ef9473be 100644
--- a/hysop/operator/tests/test_transpose.py
+++ b/hysop/operator/tests/test_transpose.py
@@ -1,9 +1,10 @@
+
 import random
 from hysop.deps import np, it
 from hysop.testsenv import __ENABLE_LONG_TESTS__, __HAS_OPENCL_BACKEND__
 from hysop.testsenv import opencl_failed, iter_clenv
 from hysop.tools.contexts import printoptions
-from hysop.tools.numerics import is_fp, is_integer
+from hysop.tools.numerics import is_fp, is_complex, is_integer
 from hysop.tools.types import check_instance, first_not_None
 from hysop.tools.io_utils import IO
 from hysop.operator.transpose import Transpose, Implementation
@@ -13,19 +14,19 @@ from hysop import Field, Box
 class TestTransposeOperator(object):
 
     @classmethod
-    def setup_class(cls,
+    def setup_class(cls, 
             enable_extra_tests=__ENABLE_LONG_TESTS__,
             enable_debug_mode=False):
 
         IO.set_default_path('/tmp/hysop_tests/test_transpose')
-
+        
         if enable_debug_mode:
             cls.size_min = 2
             cls.size_max = 6
         else:
             cls.size_min = 2
             cls.size_max = 23
-
+        
         cls.enable_extra_tests = enable_extra_tests
         cls.enable_debug_mode  = enable_debug_mode
 
@@ -33,7 +34,7 @@ class TestTransposeOperator(object):
     def teardown_class(cls):
         pass
 
-
+    
     def _test(self, dim, dtype, is_inplace,
             size_min=None, size_max=None, naxes=None):
         enable_extra_tests = self.enable_extra_tests
@@ -57,19 +58,20 @@ class TestTransposeOperator(object):
         if (naxes is not None):
             random.shuffle(all_axes)
             all_axes = all_axes[:min(naxes,len(all_axes))]
-
+        
         if dtype is None:
-            types = [np.int8, np.int16, np.int32, np.int64,
-                     np.uint8, np.uint16, np.uint32, np.uint64,
-                     np.float32, np.float64]
+            types = [#np.int8, np.int16, np.int32, np.int64,
+                     #np.uint8, np.uint16, np.uint32, np.uint64,
+                     #np.float32, np.float64,
+                     np.complex64, np.complex128]
             random.shuffle(types)
             dtype = types[0]
-
+        
         domain = Box(length=(1.0,)*dim)
-        for nb_components in (2,):
-            Fin  = Field(domain=domain, name='Fin', dtype=dtype,
+        for nb_components in (2,):  
+            Fin  = Field(domain=domain, name='Fin', dtype=dtype,  
                     nb_components=nb_components, register_object=False)
-            Fout = Field(domain=domain, name='Fout', dtype=dtype,
+            Fout = Field(domain=domain, name='Fout', dtype=dtype, 
                     nb_components=nb_components, register_object=False)
             for axes in all_axes:
                 for shape in shapes:
@@ -82,10 +84,15 @@ class TestTransposeOperator(object):
         shape = data[0].shape
         if is_integer(dtype):
             for d in data:
-                d[...] = np.random.random_integers(low=0, high=255, size=shape)
+                d[...] = np.random.random_integers(low=0, high=255, size=shape) 
         elif is_fp(dtype):
             for d in data:
-                d[...] = np.random.random(size=shape)
+                d[...] = np.random.random(size=shape) 
+        elif is_complex(dtype):
+            for d in data:
+                real = np.random.random(size=shape) 
+                imag = np.random.random(size=shape) 
+                d[...] = real + 1j*imag
         else:
             msg='Unknown dtype {}.'.format(dtype)
             raise NotImplementedError(msg)
@@ -106,7 +113,7 @@ class TestTransposeOperator(object):
         implementations = Transpose.implementations()
         ref_impl = Implementation.PYTHON
         assert ref_impl in implementations
-
+       
         # Compute reference solution
         print '  *reference PYTHON implementation.'
         transpose = Transpose(fields=fin, output_fields=fout,
@@ -115,35 +122,35 @@ class TestTransposeOperator(object):
         dfin  = transpose.get_input_discrete_field(fin)
         dfout = transpose.get_output_discrete_field(fout)
         dfin.initialize(self.__field_init, dtype=dtype)
-
+        
         if is_inplace:
             refin = tuple(df.copy() for df in dfin.buffers)
         else:
             refin = tuple(df for df in dfin.buffers)
 
         transpose.apply()
-
+        
         refout = tuple(df.copy() for df in dfout.buffers)
 
         for in_,out_ in zip(refin, refout):
             assert np.all(out_ == np.transpose(in_, axes=axes))
-
+        
         def iter_impl(impl):
             base_kwds = dict(fields=fin, output_fields=fout, variables=variables,
-                             axes=axes, implementation=impl,
+                             axes=axes, implementation=impl, 
                              name='test_transpose_{}'.format(str(impl).lower()))
             if impl is ref_impl:
-                return
+                return 
             elif impl is Implementation.OPENCL:
                 for cl_env in iter_clenv():
-                    msg='  *platform {}, device {}'.format(cl_env.platform.name.strip(),
+                    msg='  *platform {}, device {}'.format(cl_env.platform.name.strip(), 
                                                           cl_env.device.name.strip())
                     print msg
                     yield Transpose(cl_env=cl_env, **base_kwds)
             else:
                 msg='Unknown implementation to test {}.'.format(impl)
                 raise NotImplementedError(msg)
-
+        
         # Compare to other implementations
         for impl in implementations:
             for op in iter_impl(impl):
@@ -154,7 +161,7 @@ class TestTransposeOperator(object):
                 op.apply()
                 out = tuple( data.get().handle for data in dfout.data )
                 self._check_output(impl, op, refin, refout, out)
-
+    
     @classmethod
     def _check_output(cls, impl, op, refin_buffers, refout_buffers, out_buffers):
         check_instance(out_buffers, tuple, values=np.ndarray)
@@ -167,11 +174,11 @@ class TestTransposeOperator(object):
 
             if np.all(out == refout):
                 continue
-
+            
             if cls.enable_debug_mode:
                 has_nan = np.any(np.isnan(out))
                 has_inf = np.any(np.isinf(out))
-
+                
                 print
                 print 'Test output comparisson failed for component {}:'.format(i)
                 print ' *has_nan: {}'.format(has_nan)
@@ -187,9 +194,9 @@ class TestTransposeOperator(object):
                 print out
                 print
                 print
-
+            
             msg = 'Test failed on component {} for implementation {}.'.format(i, impl)
-            raise RuntimeError(msg)
+            raise RuntimeError(msg) 
 
 
     def test_2d_out_of_place(self):
@@ -197,44 +204,40 @@ class TestTransposeOperator(object):
     def test_3d_out_of_place(self):
         self._test(dim=3, dtype=None, is_inplace=False)
     def test_4d_out_of_place(self):
-        if __ENABLE_LONG_TESTS__:
-            self._test(dim=4, dtype=None, is_inplace=False)
+        self._test(dim=4, dtype=None, is_inplace=False)
     def test_upper_dimensions_out_of_place(self):
-        if __ENABLE_LONG_TESTS__:
-            for i in xrange(5,9):
-                self._test(dim=i, dtype=None, is_inplace=False,
-                           size_min=3, size_max=4, naxes=1)
-
+        for i in xrange(5,9):
+            self._test(dim=i, dtype=None, is_inplace=False,
+                    size_min=3, size_max=4, naxes=1)
+    
     def test_2d_inplace(self):
         self._test(dim=2, dtype=None, is_inplace=True)
     def test_3d_inplace(self):
         self._test(dim=3, dtype=None, is_inplace=True)
     def test_4d_inplace(self):
-        if __ENABLE_LONG_TESTS__:
-            self._test(dim=4, dtype=None, is_inplace=True)
+        self._test(dim=4, dtype=None, is_inplace=True)
     def test_upper_dimensions_inplace(self):
-        if __ENABLE_LONG_TESTS__:
-            for i in xrange(5,9):
-                self._test(dim=i, dtype=None, is_inplace=True,
-                           size_min=3, size_max=4, naxes=1)
+        for i in xrange(5,9):
+            self._test(dim=i, dtype=None, is_inplace=True,
+                    size_min=3, size_max=4, naxes=1)
 
     def perform_tests(self):
         self.test_2d_out_of_place()
-        self.test_3d_out_of_place()
         if __ENABLE_LONG_TESTS__:
+            self.test_3d_out_of_place()
             self.test_4d_out_of_place()
             self.test_upper_dimensions_out_of_place()
-
+        
         self.test_2d_inplace()
-        self.test_3d_inplace()
         if __ENABLE_LONG_TESTS__:
+            self.test_3d_inplace()
             self.test_4d_inplace()
             self.test_upper_dimensions_inplace()
-
+    
 if __name__ == '__main__':
-    TestTransposeOperator.setup_class(enable_extra_tests=False,
+    TestTransposeOperator.setup_class(enable_extra_tests=False, 
                                       enable_debug_mode=False)
-
+    
     test = TestTransposeOperator()
     test.perform_tests()
 
diff --git a/hysop/operator/transpose.py b/hysop/operator/transpose.py
index 5704673cbef2d51fb4a6a571a3b350ea2a79d1ea..2ae2e42b02a65407e03cced31e4b708c000fa6db 100644
--- a/hysop/operator/transpose.py
+++ b/hysop/operator/transpose.py
@@ -153,9 +153,10 @@ class Transpose(ComputationalGraphNodeGenerator):
         candidate_output_tensors = filter(lambda x: x.is_tensor, output_fields)
         
         base_kwds = base_kwds or dict()
-        super(Transpose,self).__init__(name=name, candidate_input_tensors=candidate_input_tensors,
-                                                  candidate_output_tensors=candidate_output_tensors,
-                                                  **base_kwds)
+        super(Transpose,self).__init__(name=name, 
+                candidate_input_tensors=candidate_input_tensors,
+                candidate_output_tensors=candidate_output_tensors,
+                **base_kwds)
         
         # expand tensors
         ifields, ofields = (), ()
@@ -215,14 +216,16 @@ class Transpose(ComputationalGraphNodeGenerator):
             raise ValueError(msg)
 
         for (input_field, output_field) in zip(input_fields, output_fields):
-            in_topo_descriptor  = ComputationalGraphNode.get_topo_descriptor(variables, input_field)
-            out_topo_descriptor = ComputationalGraphNode.get_topo_descriptor(variables, output_field)
+            in_topo_descriptor  = ComputationalGraphNode.get_topo_descriptor(
+                    variables, input_field)
+            out_topo_descriptor = ComputationalGraphNode.get_topo_descriptor(
+                    variables, output_field)
             if (input_field.domain != output_field.domain):
                 msg = 'input_field {} and output_field {} do not share the same domain.'
                 msg.format(input_field.name, output_field.name)
                 raise ValueError(msg)
             idim = input_field.domain.dim
-            if idim != dim:
+            if (idim != dim):
                 msg = 'input_field {} is of dimension {} and does not match first '
                 msg += 'input_field {} dimension {}.'
                 msg.format(input_field, idim, input_fields[0].name, dim)
@@ -257,7 +260,7 @@ class Transpose(ComputationalGraphNodeGenerator):
         del axes
             
         check_instance(candidate_axes, dict, keys=tuple)
-        for axes, target_tstate in candidate_axes.iteritems():
+        for (axes, target_tstate) in candidate_axes.iteritems():
             check_instance(axes, tuple, values=(int,long))
             check_instance(target_tstate, TranspositionState[dim], allow_none=True)
             if len(axes)!=dim:
diff --git a/hysop/operators.py b/hysop/operators.py
index 1b7f4903b8172b5f4aa05f2a22581d2b29072a53..3399f0e45e0ce7eadff1856f73c463e294e02b7e 100644
--- a/hysop/operators.py
+++ b/hysop/operators.py
@@ -5,10 +5,10 @@ Allows things like:
 from hysop.operators import DirectionalAdvection
 """
 
-from hysop.operator.poisson import Poisson
-from hysop.operator.poisson_rotational import PoissonRotational
-from hysop.operator.diffusion  import Diffusion  # FFTW diffusion
-from hysop.operator.advection  import Advection  # Scales fortran advection
+from hysop.operator.poisson      import Poisson
+from hysop.operator.poisson_curl import PoissonCurl
+from hysop.operator.diffusion    import Diffusion  # FFTW diffusion
+from hysop.operator.advection    import Advection  # Scales fortran advection
 
 from hysop.operator.redistribute           import Redistribute
 from hysop.operator.analytic               import AnalyticField
@@ -28,13 +28,19 @@ from hysop.operator.dummy                  import Dummy
 from hysop.operator.custom                 import CustomOperator
 from hysop.operator.convergence            import Convergence
 
-from hysop.operator.derivative import SpaceDerivative,       \
-                                      MultiSpaceDerivatives, \
-                                      Gradient
+from hysop.operator.derivative import SpaceDerivative,                  \
+                                      SpectralSpaceDerivative,          \
+                                      FiniteDifferencesSpaceDerivative, \
+                                      MultiSpaceDerivatives
 
-from hysop.operator.min_max import MinMaxFieldStatistics,      \
-                                   MinMaxDerivativeStatistics, \
-                                   MinMaxGradientStatistics
+from hysop.operator.min_max import MinMaxFieldStatistics,                       \
+                                   MinMaxFiniteDifferencesDerivativeStatistics, \
+                                   MinMaxSpectralDerivativeStatistics
+
+from hysop.operator.gradient import Gradient, MinMaxGradientStatistics
+from hysop.operator.curl     import Curl, SpectralCurl
+from hysop.operator.external_force import SpectralExternalForce
+from hysop.backend.device.opencl.operator.external_force import SymbolicExternalForce
 
 from hysop.numerics.splitting.strang import StrangSplitting
 from hysop.operator.directional.symbolic_dir   import DirectionalSymbolic
diff --git a/hysop/parameters/parameter.py b/hysop/parameters/parameter.py
index a27f41605728fc36294f1400ff33373da2374849..8bcaeafc059d4bc19a439a37f6c229ada84d5372 100644
--- a/hysop/parameters/parameter.py
+++ b/hysop/parameters/parameter.py
@@ -118,7 +118,7 @@ class Parameter(TaggedObject, VariableTag):
             value = value._get_value()
         self._set_value_impl(value)
         if not self.quiet:
-            msg='>Parameter {} set to {}.'.format(self.pretty_name, value)
+            msg='>Parameter {} set to {}.'.format(self.pretty_name, self._value)
             vprint(msg)
 
     def _get_value(self):
diff --git a/hysop/problem.py b/hysop/problem.py
index 6101a4da0c66c5c200d1982395db4bb80881d88e..a027ee8774f516132dc0c9c6131503ab6e70ea4f 100644
--- a/hysop/problem.py
+++ b/hysop/problem.py
@@ -1,4 +1,4 @@
-import datetime
+import datetime, sys
 from hysop.constants import Backend, MemoryOrdering
 from hysop.tools.string_utils import vprint_banner
 from hysop.tools.contexts import Timer
@@ -18,27 +18,53 @@ class Problem(ComputationalGraph):
         self.push_nodes(*ops)
 
     @debug
-    def build(self, allow_subbuffers=False):
+    def build(self, args=None, allow_subbuffers=False):
         with Timer() as tm:
-            vprint('\nInitializing problem...')
-            # Initialize and Discretize first the other Problems
-            for node in [_ for _ in self.nodes if isinstance(_, Problem)]:
-                node.initialize(outputs_are_inputs=True, topgraph_method=None)
-                node.discretize()
-            self.initialize(outputs_are_inputs=True, topgraph_method=None)
-            vprint('\nDiscretizing problem...')
-            self.discretize()
-            vprint('\nGetting work properties...')
-            work = self.get_work_properties()
-            vprint('\nAllocating work...')
-            work.allocate(allow_subbuffers=allow_subbuffers)
-            vprint('\nSetting up problem...')
-            self.setup(work)
+            msg = self.build_problem(args=args, allow_subbuffers=allow_subbuffers)
+            if msg:
+                msg=' Problem {} achieved, exiting ! '.format(msg)
+                vprint_banner(msg, at_border=2)
+                sys.exit(0)
         msg=' Problem building took {} ({}s) '
         msg=msg.format(datetime.timedelta(seconds=round(tm.interval)),
                         tm.interval)
         vprint_banner(msg, spacing=True, at_border=2)
 
+        if (args is not None) and args.stop_at_build:
+            msg=' Problem has been built, exiting. '
+            vprint_banner(msg, at_border=2)
+            sys.exit(0)
+
+    def build_problem(self, args, allow_subbuffers):
+        if (args is not None) and args.stop_at_initialization:
+            return 'initialization'
+        vprint('\nInitializing problem...')
+        # Initialize and Discretize first the other Problems
+        for node in [_ for _ in self.nodes if isinstance(_, Problem)]:
+            node.initialize(outputs_are_inputs=True, topgraph_method=None)
+            node.discretize()
+        self.initialize(outputs_are_inputs=True, topgraph_method=None)
+
+        if (args is not None) and args.stop_at_discretization:
+            return 'discretization'
+        vprint('\nDiscretizing problem...')
+        self.discretize()
+
+        if (args is not None) and args.stop_at_work_properties:
+            return 'work properties retrieval'
+        vprint('\nGetting work properties...')
+        work = self.get_work_properties()
+
+        if (args is not None) and args.stop_at_work_allocation:
+            return 'work allocation'
+        vprint('\nAllocating work...')
+        work.allocate(allow_subbuffers=allow_subbuffers)
+
+        if (args is not None) and args.stop_at_setup:
+            return 'setup'
+        vprint('\nSetting up problem...')
+        self.setup(work)
+
     def discretize(self):
         super(Problem, self).discretize()
         if self._do_check_unique_clenv:
diff --git a/hysop/symbolic/__init__.py b/hysop/symbolic/__init__.py
index 6a25c64c9b7ec429c07f1b7b1eb4438ab9da03d1..3351927873e2c60548aa0b11b77895683b828825 100644
--- a/hysop/symbolic/__init__.py
+++ b/hysop/symbolic/__init__.py
@@ -1,7 +1,7 @@
 
 import sympy as sm
 from hysop.tools.sympy_utils import Symbol, Dummy, Expr, AppliedUndef, UndefinedFunction
-from hysop.tools.sympy_utils import subscript, subscripts, xsymbol
+from hysop.tools.sympy_utils import subscript, subscripts, xsymbol, freq_symbol
 
 class TimeSymbol(Dummy):
     """Tag for space symbols."""
@@ -25,6 +25,14 @@ space_symbols = tuple(SpaceSymbol(
         for i in xrange(16))
 """Dummy symbols representing space."""
 
+freq_symbols = tuple(SpaceSymbol(
+    name='nu{}'.format(i),
+    pretty_name=freq_symbol+subscript(i), 
+    var_name='nu{}'.format(i),
+    latex_name='{{\nu}}_{{{}}}'.format(i))
+        for i in xrange(16))
+"""Dummy symbols representing wave numbers."""
+
 dspace_symbols = tuple(SpaceSymbol(
     name='dx_{}'.format(i),
     pretty_name=u'd'+xsymbol+subscript(i), 
diff --git a/hysop/symbolic/array.py b/hysop/symbolic/array.py
index 013a8948845f5139e5371527bac86cab8b2432af..72f0e05c29e819c44135048d79cf82f15c92c2dc 100644
--- a/hysop/symbolic/array.py
+++ b/hysop/symbolic/array.py
@@ -1,13 +1,14 @@
 
 from abc import ABCMeta, abstractmethod
-from hysop.symbolic.base import SymbolicScalar, sm
+from hysop.constants import Backend
+from hysop.symbolic.base import DummySymbolicScalar, sm
 from hysop.tools.types import check_instance, to_tuple, first_not_None
 from hysop.tools.numpywrappers import npw
 from hysop.backend.device.opencl import clArray
 from hysop.backend.device.opencl.opencl_array import OpenClArray
 from hysop.backend.host.host_array import HostArray
 
-class SymbolicMemoryObject(SymbolicScalar):
+class SymbolicMemoryObject(DummySymbolicScalar):
     def __new__(cls, memory_object, name, **kwds):
         obj = super(SymbolicMemoryObject, cls).__new__(cls, name=name, **kwds)
         obj._memory_object = None
@@ -35,11 +36,12 @@ class SymbolicMemoryObject(SymbolicScalar):
     @abstractmethod
     def bind_memory_object(self, memory_object):
         if (self._memory_object is not None):
-            msg='An memory_object was already bind to SymbolicArray {}.'.format(self.name)
+            msg='A memory_object has already been bound to SymbolicArray {}.'.format(self.name)
             raise RuntimeError(msg)
         if isinstance(memory_object, (OpenClArray, HostArray)):
             memory_object = memory_object.handle
         self._memory_object = memory_object
+        return self
 
     @property
     def shape(self):
@@ -67,12 +69,35 @@ class SymbolicMemoryObject(SymbolicScalar):
         self.assert_bound()
         return self._memory_object.ndim
 
+    def short_description(self):
+        self.assert_bound()
+        return '{}[dim={}, shape=[], strides={}, dtype={}]'.format(
+                self.__class__.__name__,
+                self.dim, self.shape, self.strides, self.dtype)
+    
+    def __eq__(self, other):
+        return id(self) == id(other)
+    def __hash__(self):
+        return id(self)
+    def _hashable_content(self):
+        """See sympy.core.basic.Basic._hashable_content()"""
+        hc = super(SymbolicMemoryObject, self)._hashable_content()
+        hc += (str(id(self)),)
+        return hc
+
 
 class IndexedBuffer(sm.Indexed):
     """
     Tag for indexed SymbolicBuffers.
     """
-    pass
+    @property
+    def indexed_object(self):
+        return self.args[0].args[0]
+
+    @property
+    def index(self):
+        assert len(self.args)==2
+        return self.args[1]
 
 class SymbolicArray(SymbolicMemoryObject):
     """
@@ -123,6 +148,17 @@ class SymbolicArray(SymbolicMemoryObject):
     def dim(self):
         return self._dim
 
+    def to_backend(self, backend):
+        if (backend is Backend.HOST):
+            self.__class__ = HostSymbolicArray
+        elif (backend is Backend.OPENCL):
+            self.__class__ = OpenClSymbolicArray
+        else:
+            msg='Unknown backend kind {}.'.format(backend)
+            raise NotImplementedError(msg)
+        return self
+
+
 class SymbolicBuffer(SymbolicMemoryObject):
     """
     A buffer will not be indexed by local indices by default.
@@ -136,16 +172,28 @@ class SymbolicBuffer(SymbolicMemoryObject):
     @property
     def buffer(self):
         return self._memory_object
+    
+    def to_backend(self, backend):
+        if (backend is Backend.HOST):
+            self.__class__ = HostSymbolicBuffer
+        elif (backend is Backend.OPENCL):
+            self.__class__ = OpenClSymbolicBuffer
+        else:
+            msg='Unknown backend kind {}.'.format(backend)
+            raise NotImplementedError(msg)
+        return self
+
 
 class SymbolicHostMemoryObject(object):
     def bind_memory_object(self, memory_object):
         check_instance(memory_object, (HostArray, npw.ndarray))
-        super(SymbolicHostMemoryObject, self).bind_memory_object(memory_object)
+        return super(SymbolicHostMemoryObject, self).bind_memory_object(memory_object)
+
 
 class SymbolicDeviceMemoryObject(object):
     def bind_memory_object(self, memory_object):
         check_instance(memory_object, (OpenClArray, clArray.Array))
-        super(SymbolicDeviceMemoryObject, self).bind_memory_object(memory_object)
+        return super(SymbolicDeviceMemoryObject, self).bind_memory_object(memory_object)
     @property
     def base_data(self):
         self.assert_bound()
@@ -155,6 +203,7 @@ class SymbolicDeviceMemoryObject(object):
         self.assert_bound()
         return self._memory_object.offset
 
+
 class HostSymbolicArray(SymbolicHostMemoryObject, SymbolicArray):
     pass
 class OpenClSymbolicArray(SymbolicDeviceMemoryObject, SymbolicArray):
diff --git a/hysop/symbolic/complex.py b/hysop/symbolic/complex.py
index 05843ea3e21596ae65ba1c1f8a7e811e0d979384..339e0375524e2a7a6021029e7d9dc26ad50204a9 100644
--- a/hysop/symbolic/complex.py
+++ b/hysop/symbolic/complex.py
@@ -9,10 +9,10 @@ class ComplexMul(sm.Expr):
         return obj
 
     def __str__(self):
-        return '{}*{}'.format(self.lhs, self.rhs)
+        return 'cmul({},{})'.format(self.lhs, self.rhs)
     
     def __repr__(self):
         return 'ComplexMul({},{})'.format(repr(self.lhs), repr(self.rhs))
     
     def _sympystr(self, printer):
-        return '{}*{}'.format(printer._print(self.lhs), printer._print(self.rhs))
+        return 'cmul({},{})'.format(printer._print(self.lhs), printer._print(self.rhs))
diff --git a/hysop/symbolic/field.py b/hysop/symbolic/field.py
index b9b498e08493fa118993ff9df9b13eb7f1c6ca7b..85026b2d30c9bc19bb200dddebc3dc7b46a43b12 100644
--- a/hysop/symbolic/field.py
+++ b/hysop/symbolic/field.py
@@ -1,7 +1,11 @@
-
+from abc import abstractmethod
 from hysop.deps import sm
+from hysop.constants import BoundaryCondition
+
 from hysop.tools.numpywrappers import npw
 from hysop.tools.types import check_instance, first_not_None
+from hysop.tools.sympy_utils import get_derivative_variables
+from hysop.tools.numerics import find_common_dtype
         
 from hysop.fields.continuous_field import Field, TensorField
 from hysop.fields.discrete_field import DiscreteField, DiscreteTensorField
@@ -9,16 +13,214 @@ from hysop.symbolic import Symbol
 from hysop.symbolic.base import TensorBase, SymbolicTensor
 from hysop.symbolic.func import UndefinedFunction, AppliedSymbolicFunction, FunctionBase, \
                                 SymbolicFunctionTensor
+from hysop.domain.domain import Domain
+
+class FieldExpressionI(object):
+    @abstractmethod
+    def lboundaries(self):
+        pass
+    
+    @abstractmethod
+    def rboundaries(self):
+        pass
+
+    @abstractmethod
+    def domain(self):
+        pass
+    
+    @abstractmethod
+    def dtype(self):
+        pass
+    
+    @property
+    def boundaries(self):
+        return (self.lboundaries, self.rboundaries)
+    
+    def format_boundaries(self):
+        from hysop.constants import format_boundaries as fb
+        return fb(*self.boundaries)
+   
+
+class FieldExpression(FieldExpressionI):
+    def __init__(self, *args, **kwds):
+        self._domain      = kwds.pop('domain', None)
+        self._dtype       = kwds.pop('dtype', None)
+        self._lboundaries = kwds.pop('lboundaries', None)
+        self._rboundaries = kwds.pop('rboundaries', None)
+        super(FieldExpression, self).__init__(*args, **kwds)
+    
+    @property
+    def lboundaries(self):
+        assert (self._lboundaries is not None)
+        return self._lboundaries
+    @lboundaries.setter
+    def lboundaries(self, lb):
+        check_instance(lb, npw.ndarray, values=BoundaryCondition, 
+                            size=self.domain.dim, ndim=1)
+        self._lboundaries = lb
+    
+    @property
+    def rboundaries(self):
+        assert (self._rboundaries is not None)
+        return self._rboundaries
+    @rboundaries.setter
+    def rboundaries(self, rb):
+        check_instance(rb, npw.ndarray, values=BoundaryCondition, 
+                            size=self.domain.dim, ndim=1)
+        self._rboundaries = rb
+
+    @property
+    def domain(self):
+        assert (self._domain is not None)
+        return self._domain
+    @domain.setter
+    def domain(self, dom):
+        assert (self._domain is None)
+        check_instance(dom, Domain)
+        self._domain = dom 
+
+    @property
+    def dtype(self):
+        assert (self._dtype is not None)
+        return self._dtype
+    @dtype.setter
+    def dtype(self, dt):
+        assert (self._dtype is None)
+        check_instance(dt, npw.dtype)
+        self._dtype = dt 
+
+
+class FieldExpressionBuilder(object):
+    class BoundaryIncompatibilityError(ValueError):
+        pass
+    class InvalidExpression(ValueError):
+        pass
+
+    @classmethod
+    def is_field_expr(cls, expr):
+        return isinstance(expr, FieldExpressionI)
+
+    @classmethod
+    def update_boundaries(cls, boundary, order):
+        from hysop.constants import BoundaryCondition
+        if (order%2)==0:
+            return boundary
+        elif (boundary is BoundaryCondition.PERIODIC):
+            return BoundaryCondition.PERIODIC
+        elif (boundary is BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+            return BoundaryCondition.HOMOGENEOUS_NEUMANN
+        elif (boundary is BoundaryCondition.HOMOGENEOUS_NEUMANN):
+            return BoundaryCondition.HOMOGENEOUS_DIRICHLET
+        else:
+            msg='FATAL ERROR: Unknown boundary condition {}.'
+            msg=msg.format(bd)
+            raise NotImplementedError(msg)
+
+    @classmethod
+    def to_field_expression(cls, expr, space_symbols, strict=True):
+        check_instance(expr, sm.Expr)
+        def _to_field_expression_impl(expr):
+            if cls.is_field_expr(expr):
+                return expr
+            elif isinstance(expr, sm.Derivative):
+                class DerivativeFieldExpr(FieldExpression, sm.Derivative):
+                    pass
+                e = _to_field_expression_impl(expr.args[0])
+                if cls.is_field_expr(e):
+                    dtype, domain = e.dtype, e.domain
+                    lb, rb = e.lboundaries.copy(), e.rboundaries.copy(), 
+                    assert len(space_symbols)==lb.size==rb.size
+                    for xi in get_derivative_variables(expr):
+                        assert xi in space_symbols, xi
+                        i = space_symbols[::-1].index(xi)
+                        lb[i] = cls.update_boundaries(lb[i], +1)
+                        rb[i] = cls.update_boundaries(rb[i], +1)
+                    expr = DerivativeFieldExpr(e, *expr.args[1:])
+                    expr.domain = domain
+                    expr.dtype  = dtype
+                    expr.lboundaries = lb
+                    expr.rboundaries = rb
+                    return expr
+                else:
+                    return expr
+            else:
+                func = expr.func
+                args = tuple(_to_field_expression_impl(a) for a in expr.args)
+                field_expression_args = tuple(filter(lambda x: cls.is_field_expr(x), args))
+                if field_expression_args:
+                    try:
+                        return cls.make_expr(func, *args)
+                    except cls.BoundaryIncompatibilityError:
+                        msg='\nError during the handling of expression {}.'.format(expr)
+                        msg+='\nSome boundaries were not compatible:'
+                        msg+='\n  *'+'\n  *'.join('{}: {}'.format(a, a.format_boundaries()) 
+                                                for a in field_expression_args)
+                        raise cls.BoundaryIncompatibilityError(msg)
+                else:
+                    return expr
+        fexpr = _to_field_expression_impl(expr)
+        if strict and (not cls.is_field_expr(fexpr)):
+            msg='\nError during the handling of expression {}.'.format(expr)
+            msg+='\nCould not determine boundaries because no FieldExpression '
+            msg+='was present in expression.'
+            raise cls.InvalidExpression(msg)
+        return fexpr
+
+    @classmethod
+    def make_expr(cls, func, *args):
+        check_instance(func, type)
+        field_expression_args = tuple(filter(lambda x: cls.is_field_expr(x), args))
+        if not field_expression_args:
+            msg='No FieldExpression arguments present in args.'
+            raise ValueError(msg)
+        if not cls.check_boundary_compatibility(*field_expression_args):
+            raise cls.BoundaryIncompatibilityError
+        fea0 = field_expression_args[0]
+        new_func = type(func.__name__+'FieldExpr', (FieldExpression, func), {})
+        new_expr = new_func(*args)
+        new_expr.dtype       = npw.dtype(find_common_dtype(*tuple(a.dtype for a in field_expression_args)))
+        new_expr.domain      = fea0.domain
+        new_expr.lboundaries = fea0.lboundaries.copy()
+        new_expr.rboundaries = fea0.rboundaries.copy()
+        return new_expr
+        
+
+    @classmethod
+    def check_boundary_compatibility(cls, arg0, *args):
+        check_instance(args, tuple, values=FieldExpressionI)
+        domain, lb, rb = arg0.domain, arg0.lboundaries, arg0.rboundaries
+        if args:
+            match  = all((domain == a.domain)   for a in args)
+            match &= all(all(lb==a.lboundaries) for a in args)
+            match &= all(all(rb==a.rboundaries) for a in args)
+            return match
+        else:
+            return True
+
+        
 
 class FieldBase(FunctionBase):
-    def __new__(cls, field, idx=None, name=None, pretty_name=None, **kwds):
+
+    def _sympy_(self):
+        '''for sympify'''
+        return self
+
+    def __new__(cls, field, idx=None, 
+                    **kwds):
+        assert 'name' not in kwds
+        assert 'pretty_name' not in kwds
+        assert 'latex_name' not in kwds
+        assert 'var_name' not in kwds
         check_instance(field, (Field, DiscreteField))
         assert (field.nb_components == 1) or (idx is not None), (field.nb_components, idx)
         index = first_not_None(idx, [0])[0]
-        name = first_not_None(name, field.name)
-        pretty_name = first_not_None(pretty_name, field.pretty_name)
+        name        = field.name
+        pretty_name = field.pretty_name
+        var_name    = field.var_name
+        latex_name  = field.latex_name
         assert (0<=index<field.nb_components), index
-        obj = super(FieldBase, cls).__new__(cls, name=name, pretty_name=pretty_name, **kwds)
+        obj = super(FieldBase, cls).__new__(cls, name=name, pretty_name=pretty_name, 
+                                                 var_name=var_name, latex_name=latex_name, **kwds)
         obj._field = field
         obj._index = index
         return obj
@@ -28,7 +230,7 @@ class FieldBase(FunctionBase):
         hc = super(FieldBase, self)._hashable_content()
         hc += (self._field, self._index,)
         return hc
-
+    
     @property
     def field(self):
         """Get associated field."""
@@ -52,9 +254,8 @@ class SymbolicDiscreteField(FieldBase, Symbol):
     """
     def __new__(cls, field, name=None, fn=None, **kwds):
         check_instance(field, DiscreteField)
-        name  = first_not_None(name, field.name)
         return super(SymbolicDiscreteField, cls).__new__(cls, field=field, 
-                name=name, fn=fn, **kwds)
+                fn=fn, **kwds)
 
     @classmethod
     def from_field(cls, field):
@@ -95,12 +296,16 @@ class SymbolicField(FieldBase, UndefinedFunction):
         return not (self==other)
 
 
-class AppliedSymbolicField(AppliedSymbolicFunction):
+class AppliedSymbolicField(FieldExpressionI, AppliedSymbolicFunction):
     """Applied scalar fields, hold a reference to a continuous field."""
     def __new__(cls, *args, **kwds):
         args = args if args else cls.field.domain.frame.vars
         return super(AppliedSymbolicField, cls).__new__(cls, *args, **kwds)
     
+    def _sympy_(self):
+        '''for sympify'''
+        return self
+    
     def _hashable_content(self):
         """See sympy.core.basic.Basic._hashable_content()"""
         hc = super(AppliedSymbolicField, self)._hashable_content()
@@ -120,6 +325,23 @@ class AppliedSymbolicField(AppliedSymbolicFunction):
     def indexed_field(self):
         """Get a unique identifier for an indexed field component."""
         return (self.field, self.index)
+    
+    @property
+    def lboundaries(self):
+        return self.field.lboundaries
+    
+    @property
+    def rboundaries(self):
+        return self.field.rboundaries
+
+    @property
+    def domain(self):
+        return self.field.domain
+    
+    @property
+    def dtype(self):
+        return self.field.dtype
+
 
 class SymbolicFieldTensor(SymbolicFunctionTensor):
     """Symbolic tensor symbol."""
@@ -147,7 +369,7 @@ class SymbolicDiscreteFieldTensor(TensorBase):
 def diff(F, *symbols, **assumptions):
     is_tensor = isinstance(F, npw.ndarray)
     if is_tensor:
-        return F.astype(TensorBase).diff(*symbols, **assumptions)
+        return F.view(TensorBase).diff(*symbols, **assumptions)
     else:
         return sm.diff(F, *symbols, **assumptions)
 
@@ -176,31 +398,54 @@ def grad(F, frame, axis=-1):
     return gradF.view(TensorBase)
 
 def div(F, frame, axis=-1):
-    assert isinstance(F, npw.ndarray)
-    assert F.shape[axis] == frame.dim
-    shape = F.shape
-    ndim  = F.ndim
-    axis = (axis+ndim)%ndim
-    
-    divF = npw.empty_like(F)
-    for idx in npw.ndindex(*shape):
-        divF[idx] = diff(F[idx], frame.coords[idx[axis]])
-    divF = divF.sum(axis=axis)
-    return divF.view(TensorBase)
+    if isinstance(F, npw.ndarray):
+        assert F.shape[axis] == frame.dim
+        shape = F.shape
+        ndim  = F.ndim
+        axis = (axis+ndim)%ndim
+        
+        divF = npw.empty_like(F)
+        for idx in npw.ndindex(*shape):
+            divF[idx] = diff(F[idx], frame.coords[idx[axis]])
+        divF = divF.sum(axis=axis)
+        try:
+            if divF.size==1:
+                return divF.item()
+            else:
+                return divF.view(TensorBase)
+        except AttributeError:
+            return divF
+    else:
+        assert frame.dim==1
+        return F.diff(frame.coords[0])
 
 def rot(F, frame):
+    F = npw.atleast_1d(F)
+    assert (F.ndim == 1), F.ndim
     assert (frame.dim in (2,3))
-    assert (F.size == frame.dim)
     X = frame.coords
     if (frame.dim == 2):
-        rotF = diff(F[1],X[0]) - diff(F[0],X[1])
-        return rotF
+        if (F.size == 1):
+            rotF = npw.asarray([
+                +diff(F[0], X[1]),
+                -diff(F[0], X[0]),
+            ])
+            return rotF.view(TensorBase)
+        elif (F.size == 2):
+            return diff(F[1],X[0]) - diff(F[0],X[1])
+        else:
+            raise ValueError(F.size)
+    elif (frame.dim == 3):
+        if (F.size == 3):
+            rotF = npw.empty_like(F)
+            rotF[0] = diff(F[2],X[1]) - diff(F[1],X[2])
+            rotF[1] = diff(F[0],X[2]) - diff(F[2],X[0])
+            rotF[2] = diff(F[1],X[0]) - diff(F[0],X[1])
+            return rotF.view(TensorBase)
+        else:
+            raise ValueError(F.size)
     else:
-        rotF = npw.empty_like(F)
-        rotF[0] = diff(F[2],X[1]) - diff(F[1],X[2])
-        rotF[1] = diff(F[0],X[2]) - diff(F[2],X[0])
-        rotF[2] = diff(F[1],X[0]) - diff(F[0],X[1])
-        return rotF.view(TensorBase)
+        raise ValueError(frame.dim)
 
 def curl(*args, **kwds):
     return rot(*args, **kwds)
diff --git a/hysop/symbolic/frame.py b/hysop/symbolic/frame.py
index a8743ce3889e78f663d9b665f2a17062d061f255..a3d7dda04179690d25ae13caac8d808a97665a41 100644
--- a/hysop/symbolic/frame.py
+++ b/hysop/symbolic/frame.py
@@ -1,16 +1,23 @@
 
-from hysop.tools.types import first_not_None, check_instance
-from hysop.symbolic import dspace_symbols, space_symbols, time_symbol
+from hysop.tools.types import first_not_None, check_instance, to_tuple
+from hysop.symbolic import dspace_symbols, space_symbols, freq_symbols, time_symbol
 
 class SymbolicFrame(object):
     """n-dimensional symbolic frame."""
 
-    def __init__(self, dim, **kwds):
+    def __init__(self, dim, freq_axes=None, **kwds):
         """Initialize a frame with given dimension."""
         super(SymbolicFrame, self).__init__(**kwds)
         assert dim>0, 'Incompatible dimension.'
+        
+        coords = list(space_symbols[:dim])
+        if (freq_axes is not None):
+            freq_axes = to_tuple(freq_axes)
+            for i in freq_axes:
+                coords[dim-i-1] = freq_symbols[dim-1-i]
+        self._coords = tuple(coords)
         self._dim = dim
-    
+
     @property
     def dim(self):
         """Get the dimension of this frame."""
@@ -18,8 +25,13 @@ class SymbolicFrame(object):
     
     @property
     def coords(self):
-        """Return the spatial coordinates associated to this frame."""
-        return space_symbols[:self.dim]
+        """Return the symbolic spatial coordinates associated to this frame."""
+        return self._coords
+    
+    @property
+    def freqs(self):
+        """Return the symbolic (spatial) frequency coordinates associated to this frame."""
+        return freq_symbols[:self.dim]
     
     @property
     def dcoords(self):
@@ -31,6 +43,7 @@ class SymbolicFrame(object):
         """Get the time variable for conveniance."""
         return time_symbol
     
+    
     @property
     def dtime(self):
         """Get the infinitesimal time variable for conveniance."""
diff --git a/hysop/symbolic/relational.py b/hysop/symbolic/relational.py
index e7be59db2b39708de2f0556e40e0fbdb505b5518..85529719780c25fcd3e4b11d65eae6b7fb33dbf4 100644
--- a/hysop/symbolic/relational.py
+++ b/hysop/symbolic/relational.py
@@ -200,3 +200,58 @@ class DivAugmentedAssignment(AugmentedAssignment):
 
 class ModAugmentedAssignment(AugmentedAssignment):
     _symbol = '%'
+    
+    
+
+class NAryFunction(Expr):
+    """
+    Represents relations bewteen n variables.
+
+    Parameters
+    ----------
+    args: tuple of Expr
+    """
+    @property
+    def fname(self):
+        raise NotImplemented
+
+    def __new__(cls, *exprs):
+        obj = super(NAryFunction, cls).__new__(cls, *exprs)
+        return obj
+    
+    def __str__(self):
+        return '{}({})'.format(self.fname,
+                               ', '.join(str(x) for x in self.args))
+    
+    def _sympystr(self, printer):
+        return '{}({})'.format(self.fname,
+                               ', '.join('{}'.format(printer._print(x)) for x in self.args))
+
+    def _ccode(self, printer):
+        return '{}({})'.format(self.fname,
+                               ', '.join('{}'.format(printer._print(x)) for x in self.args))
+
+    @property
+    def is_number(self):
+        return True
+        
+    @property
+    def free_symbols(self):
+        return ()
+
+
+class BinaryFunction(NAryFunction):
+    def __new__(cls, lhs, rhs):
+        return super(BinaryFunction, cls).__new__(cls, lhs, rhs)
+
+
+class Max(BinaryFunction):
+    @property
+    def fname(self):
+        return 'max'
+
+class Min(BinaryFunction):
+    @property
+    def fname(self):
+        return 'min'
+
diff --git a/hysop/symbolic/spectral.py b/hysop/symbolic/spectral.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b3b82bcfad348f18e76992ab44c2503cef582b1
--- /dev/null
+++ b/hysop/symbolic/spectral.py
@@ -0,0 +1,435 @@
+
+import sympy as sm
+import numpy as np
+
+from hysop.constants import BoundaryCondition, BoundaryExtension, TransformType
+from hysop.tools.types import check_instance, to_tuple, first_not_None
+from hysop.tools.sympy_utils import Expr, Symbol, Dummy, subscript
+from hysop.tools.spectral_utils import SpectralTransformUtils as STU
+from hysop.symbolic import SpaceSymbol
+from hysop.symbolic.array import SymbolicBuffer
+from hysop.symbolic.field import FieldExpressionBuilder, FieldExpressionI, TensorBase, \
+                                 SymbolicField, AppliedSymbolicField
+from hysop.symbolic.frame import SymbolicFrame
+from hysop.fields.continuous_field import Field, ScalarField, TensorField
+from hysop.tools.spectral_utils import SpectralTransformUtils
+
+class WaveNumberIndex(sm.Symbol):
+    def __new__(cls, axis):
+        obj = super(WaveNumberIndex, cls).__new__(cls, 'i{}'.format(axis))
+        obj.axis = axis
+        obj._axes = None
+        obj._real_index = None
+        return obj
+
+    def bind_axes(self, axes):
+        assert (self._axes is None) or (axes == self._axes)
+        dim = len(axes)
+        from hysop.symbolic import local_indices_symbols
+        self._axes = axes
+        self._real_index = local_indices_symbols[dim-1-axes.index(self.axis)]
+
+    @property
+    def real_index(self):
+        if (self._real_index is None):
+            msg='No axes bound yet !'
+            raise RuntimeError(msg)
+        return self._real_index
+
+
+class WaveNumber(Dummy):
+    """Wave number symbol for SpectralTransform derivatives (and integrals)."""
+    
+    __transform2str = {
+            TransformType.FFT:      'c2c',
+            TransformType.RFFT:     'r2c',
+            TransformType.DCT_I:    'c1',
+            TransformType.DCT_II:   'c2',
+            TransformType.DCT_III:  'c3',
+            TransformType.DCT_IV:   'c4',
+            TransformType.DST_I:    's1',
+            TransformType.DST_II:   's2',
+            TransformType.DST_III:  's3',
+            TransformType.DST_IV:   's4',
+            TransformType.IFFT:     'c2c',
+            TransformType.IRFFT:    'r2c',
+            TransformType.IDCT_I:   'c1',
+            TransformType.IDCT_II:  'c3',
+            TransformType.IDCT_III: 'c2',
+            TransformType.IDCT_IV:  'c4',
+            TransformType.IDST_I:   's1',
+            TransformType.IDST_II:  's3',
+            TransformType.IDST_III: 's2',
+            TransformType.IDST_IV:  's4',
+    }
+    
+    __wave_numbers = {}
+
+    def __new__(cls, axis, transform, exponent, **kwds):
+        check_instance(transform, TransformType)
+        check_instance(axis, int, minval=0)
+        check_instance(exponent, int, minval=1)
+        
+        if (transform is TransformType.NONE):
+            return None
+        
+        if (exponent == 0):
+            return 1
+        
+        key = (transform, axis, exponent)
+        if key in cls.__wave_numbers:
+            return cls.__wave_numbers[key]
+        
+        tr_str = cls.__transform2str[transform]
+        if len(tr_str)==2:
+            tr_pstr = tr_str[0] + subscript(int(tr_str[1]))
+        else:
+            tr_pstr = tr_str
+
+        name = 'k{}_{}'.format(axis, tr_str)
+        pretty_name = 'k'+subscript(axis)+'_'+tr_pstr
+
+        if (exponent < 0):
+            name = 'i' + name
+            pretty_name = 'i' + pretty_name
+            exponent = -exponent
+
+        if (exponent > 1):
+            name += '__{}'.format(exponent)
+            pretty_name += '__{}'.format(exponent)
+        
+        obj = super(WaveNumber, cls).__new__(cls, 
+                name=name, pretty_name=pretty_name, **kwds)
+        obj._axis      = int(axis)
+        obj._transform = transform
+        obj._exponent  = int(exponent)
+
+        cls.__wave_numbers[key] = obj
+
+        return obj
+
+    @property
+    def axis(self):
+        return self._axis
+    @property
+    def transform(self):
+        return self._transform
+    @property
+    def exponent(self):
+        return self._exponent
+    
+    @property
+    def is_real(self):
+        tr  = self._transform
+        exp = self._exponent
+        is_real = STU.is_R2R(tr)
+        is_real |= ((not STU.is_R2R(tr)) and (exp % 2 == 0))
+        return is_real
+    
+    @property
+    def is_complex(self):
+        tr  = self._transform
+        exp = self._exponent
+        return ((not STU.is_R2R(tr)) and (exp % 2 != 0))
+
+    def pow(self, exponent):
+        exponent *= self.exponent
+        return WaveNumber(axis=self.axis, transform=self.transform, exponent=exponent)
+    
+    def indexed_buffer(self, name=None):
+        name = first_not_None(name, self.name)
+        buf = SymbolicBuffer(name=name, memory_object=None)
+        idx = WaveNumberIndex(self.axis)
+        obj = buf[idx]
+        obj.Wn = self
+        return obj
+
+    def __eq__(self, other):
+        if not isinstance(other, WaveNumber):
+            return NotImplemented
+        eq =  (self.axis      == other.axis)
+        eq &= (self.transform == other.transform)
+        eq &= (self.exponent  == other.exponent)
+        return eq
+
+    def __hash__(self):
+        return hash((self.axis, self.transform, self.exponent))
+
+
+class AppliedSpectralTransform(AppliedSymbolicField):
+    """
+    An applied spectral transform.
+    """
+    def short_description(self):
+        ss = '{}(field={}, axes={}, is_forward={}, transforms=[{}])'
+        return ss.format(self.__class__.__name__,
+                self.field.pretty_name, self.transformed_axes, 
+                '1' if self.is_forward else '0',
+                self.format_transforms())
+
+    def long_description(self):
+        ss = \
+'''
+== {} ==
+  *field:            {}
+  *transformed_axes: {}
+  *spatial_axes:     {}
+  *is_forward:       {}
+  *transforms:       {}
+  *freq_vars:        {}
+  *space_vars:       {}
+  *all_vars:         {}
+  *wave_numbers:     {}
+'''
+        return ss.format(self.__class__.__name__,
+                         self.field.short_description(),
+                         self.transformed_axes,
+                         self.spatial_axes,
+                         self.is_forward,
+                         self.transforms,
+                         self.space_vars,
+                         self.freq_vars,
+                         self.all_vars,
+                         self.wave_numbers)
+
+    def format_transforms(self):
+        transforms = self.transforms
+        return ' x '.join(str(tr) for tr in transforms)
+    
+    @property
+    def field(self):
+        return self._field
+    @property
+    def transformed_axes(self):
+        return self._transformed_axes
+    @property
+    def spatial_axes(self):
+        return self._spatial_axes
+    @property
+    def freq_vars(self):
+        return self._freq_vars
+    @property
+    def space_vars(self):
+        return self._space_vars
+    @property
+    def all_vars(self):
+        return self._all_vars
+    @property
+    def frame(self):
+        return self._frame
+    
+    @property
+    def lboundaries(self):
+        return self._field.lboundaries
+    @property
+    def rboundaries(self):
+        return self._field.rboundaries
+    @property
+    def domain(self):
+        return self._field.domain
+    @property
+    def dtype(self):
+        return self._field.dtype
+    
+    @property
+    def transforms(self):
+        return self._transforms
+    @property
+    def wave_numbers(self):
+        return self._wave_numbers
+    @property
+    def is_forward(self):
+        return self._is_forward
+
+
+    # SYMPY INTERNALS ################
+    @property
+    def is_number(self):
+        return False
+
+    @property
+    def free_symbols(self):
+        return set(self._all_vars)
+
+    def _eval_derivative(self, v):
+        if v in self._freq_vars:
+            i = self._all_vars.index(v)
+            return self._wave_numbers[i]*self
+        return sm.Derivative(self, v)
+    
+    def _hashable_content(self):
+        """See sympy.core.basic.Basic._hashable_content()"""
+        hc = super(AppliedSpectralTransform, self)._hashable_content()
+        hc += (self.__class__,)
+        return hc
+    
+    def __hash__(self):
+        h = super(AppliedSpectralTransform, self).__hash__()
+        for hc in (self.__class__,):
+            h ^= hash(h)
+        return h
+    
+    def __eq__(self, other):
+        "Fix sympy v1.2 eq"
+        eq = super(AppliedSpectralTransform, self).__eq__(other)
+        if (eq is not True):
+            return eq
+        eq &= (self.__class__ is other.__class__)
+        return eq
+
+    def __ne__(self, other):
+        "Fix sympy v1.2 neq"
+        return not (self==other)
+    ###################################
+
+
+class SpectralTransform(SymbolicField):
+    """
+    A single spectral transform that may be applied.
+    This object can also be used as am sympy expression (and a FieldExpression).
+
+    This expression carries datatype and boundary conditions.
+    """
+    def __new__(cls, field, axes=None, forward=True):
+        if isinstance(field, TensorField):
+            T = field.new_empty_array()
+            wave_numbers = ()
+            for (idx, f) in field.nd_iter():
+                T[idx] = cls(field=f, axes=axes, forward=forward)
+                wave_numbers += T[idx].wave_numbers
+            T = T.view(TensorBase)
+            T.frame = T[0].frame
+            return T
+
+        dim = field.dim
+
+        check_instance(field, ScalarField)
+        axes = to_tuple(first_not_None(axes, range(field.dim)))
+        check_instance(axes, tuple, values=int, minval=0, 
+                                    maxval=dim-1, minsize=1)
+        
+        transformed_axes = tuple(sorted(set(axes)))
+        spatial_axes     = tuple(sorted(set(range(field.dim)) - set(axes)))
+        
+        frame = field.domain.frame
+        freq_vars  = tuple(frame.freqs[dim-1-i]  for i in transformed_axes[::-1])
+        space_vars = tuple(frame.coords[dim-1-i] for i in spatial_axes[::-1])
+        
+        all_vars = ()
+        for i in xrange(dim):
+            if i in transformed_axes:
+                all_vars += (frame.freqs[dim-1-i],)
+            else:
+                all_vars += (frame.coords[dim-1-i],)
+        all_vars = all_vars[::-1]
+        
+        transforms = SpectralTransformUtils.transforms_from_field(field, 
+                transformed_axes=transformed_axes)
+        for i in xrange(frame.dim):
+            assert (transforms[i] is TransformType.NONE) ^ (i in transformed_axes)
+
+        wave_numbers = cls.generate_wave_numbers(transforms)[::-1]
+        if not forward:
+            transforms = SpectralTransformUtils.get_inverse_transforms(*transforms)
+
+        frame = SymbolicFrame(dim=field.dim, freq_axes=transformed_axes)
+        assert frame.coords == all_vars
+        
+        obj = super(SpectralTransform, cls).__new__(cls, field=field,
+                bases=(AppliedSpectralTransform,))
+        obj._field = field
+        obj._transformed_axes = transformed_axes
+        obj._spatial_axes = spatial_axes
+        obj._freq_vars = freq_vars
+        obj._space_vars = space_vars
+        obj._is_forward = forward
+        obj._all_vars = all_vars
+        obj._transforms = transforms
+        obj._wave_numbers = wave_numbers
+        obj._frame = frame
+        return obj(*all_vars)
+
+    @classmethod
+    def generate_wave_numbers(cls, transforms):
+        return SpectralTransformUtils.generate_wave_numbers(*transforms)
+    
+    def _hashable_content(self):
+        """See sympy.core.basic.Basic._hashable_content()"""
+        hc = super(SpectralTransform, self)._hashable_content()
+        hc += (self._transformed_axes, self._is_forward)
+        return hc
+    
+    def __hash__(self):
+        "Fix sympy v1.2 hashes"
+        h = super(SpectralTransform, self).__hash__()
+        for hc in (self._transformed_axes, self._is_forward):
+            h ^= hash(hc)
+        return h
+
+    def __eq__(self, other):
+        "Fix sympy v1.2 eq"
+        eq = super(SpectralTransform, self).__eq__(other)
+        if (eq is not True):
+            return eq
+        for (lhc,rhc) in zip((self._transformed_axes, self._is_forward), 
+                             (other._transformed_axes, other._is_forward)):
+            eq &= (lhc == rhc)
+        return eq
+
+    def __ne__(self, other):
+        "Fix sympy v1.2 neq"
+        return not (self==other)
+
+
+if __name__ == '__main__':
+    from hysop.tools.sympy_utils import sstr
+    from hysop import Box
+    from hysop.constants import BoxBoundaryCondition
+    from hysop.defaults import VelocityField, VorticityField
+    from hysop.symbolic.field import laplacian, curl
+    from hysop.symbolic.relational import Assignment
+    from hysop.tools.sympy_utils import Greak
+    
+    dim = 3
+    d = Box(dim=dim, lboundaries=(BoxBoundaryCondition.SYMMETRIC,
+                                  BoxBoundaryCondition.OUTFLOW, 
+                                  BoxBoundaryCondition.SYMMETRIC),
+                     rboundaries=(BoxBoundaryCondition.SYMMETRIC, 
+                                  BoxBoundaryCondition.OUTFLOW,
+                                  BoxBoundaryCondition.OUTFLOW))
+
+    U = VelocityField(domain=d)
+    W = VorticityField(velocity=U)
+    psi = W.field_like(name='psi', pretty_name=Greak[23])
+    
+    W_hat   = SpectralTransform(W, forward=True)
+    U_hat   = SpectralTransform(U, forward=False)
+    psi_hat = SpectralTransform(psi)
+
+    eqs = laplacian(psi_hat, psi_hat.frame) - W_hat
+    sol = sm.solve(eqs, psi_hat.tolist())
+    sol = curl(psi_hat, psi_hat.frame).xreplace(sol)
+
+    print 'VELOCITY'
+    print U.short_description()
+    print
+    print 'VORTICITY'
+    print W.short_description()
+    print
+    print 'W_hat'
+    print W_hat
+    print
+    print 'U_hat'
+    print U_hat
+    print
+    print 'Psi_hat'
+    print psi_hat
+    print
+    for eq in Assignment.assign(U_hat, sol):
+        eq, trs, wn = SpectralTransformUtils.parse_expression(eq)
+        print
+        print eq
+        for tr in trs:
+            print tr.short_description()
+        print wn
+
diff --git a/hysop/testsenv.py b/hysop/testsenv.py
index 26ab09f70397334eafd1fc960acedb2159ece5f8..8e1b7e1ec3f10ee6b186d3afd284d15d568d1389 100644
--- a/hysop/testsenv.py
+++ b/hysop/testsenv.py
@@ -4,9 +4,13 @@ import os
 import pytest
 import shutil
 import contextlib
+import numpy as np
+import sympy as sm
 
-from hysop import __FFTW_ENABLED__, __SCALES_ENABLED__, __ENABLE_LONG_TESTS__, __TEST_ALL_OPENCL_PLATFORMS__
-from hysop.tools.types import check_instance, first_not_None
+from hysop import __FFTW_ENABLED__, __SCALES_ENABLED__, __ENABLE_LONG_TESTS__, \
+                  __TEST_ALL_OPENCL_PLATFORMS__, \
+                  __DEFAULT_PLATFORM_ID__, __DEFAULT_DEVICE_ID__
+from hysop.tools.types import check_instance, first_not_None, to_tuple
 from hysop.tools.io_utils import IO
 from hysop.tools.decorators import static_vars
 from hysop.tools.contexts import printoptions
@@ -40,6 +44,22 @@ def test_context():
                       formatter={'float': lambda x: '{:>6.2f}'.format(x)}):
         yield
 
+
+def domain_boundary_iterator(dim):
+    import numpy as np
+    import itertools as it
+    from hysop.constants import BoxBoundaryCondition
+    choices = (BoxBoundaryCondition.OUTFLOW, BoxBoundaryCondition.SYMMETRIC)
+    choices = tuple(it.product(choices, repeat=2))
+    for i in xrange(dim,-1,-1):
+        bd0 = ((BoxBoundaryCondition.PERIODIC, BoxBoundaryCondition.PERIODIC),)*i
+        for bd1 in it.product(choices, repeat=dim-i):
+            bd = bd0+bd1
+            lbd = np.asarray([x[0] for x in bd])
+            rbd = np.asarray([x[1] for x in bd])
+            yield (lbd, rbd)
+
+
 # accept failing tests when opencl is not present
 from hysop.backend import __HAS_OPENCL_BACKEND__
 if __HAS_OPENCL_BACKEND__:
@@ -50,30 +70,56 @@ if __HAS_OPENCL_BACKEND__:
         """
         return f
         
-    @static_vars(cl_environments=[])
-    def iter_clenv(**kwds):
+    @static_vars(cl_environments={})
+    def iter_clenv(device_type=None, all_platforms=None, **kwds):
         """
         Iterate over all platforms and device and yield OpenClEnvironments.
         If __ENABLE_LONG_TESTS__ is False, just yield the default OpenCl
         environment.
         """
+        all_platforms = first_not_None(all_platforms, __TEST_ALL_OPENCL_PLATFORMS__)
+        if isinstance(device_type, str):
+            if (device_type=='cpu'):
+                cl_device_type = cl.device_type.CPU
+            elif (device_type=='gpu'):
+                cl_device_type = cl.device_type.GPU
+            else:
+                raise NotImplementedError(device_type)
+        else:
+            cl_device_type = device_type
+
         cl_environments = iter_clenv.cl_environments
-        if not cl_environments:
-            mpi_params = default_mpi_params()
-            if __TEST_ALL_OPENCL_PLATFORMS__:
-                for i,plat in enumerate(cl.get_platforms()):
-                    for j,dev in enumerate(plat.get_devices()):
-                        cl_env = get_or_create_opencl_env(platform_id=i, device_id=j, 
-                                mpi_params=mpi_params, **kwds)
-                        cl_environments.append(cl_env)
+        if (cl_device_type not in cl_environments):
+            cl_environments[cl_device_type] = []
+            if (cl_device_type is None):
+                mpi_params = default_mpi_params()
+                if all_platforms:
+                    for i,plat in enumerate(cl.get_platforms()):
+                        for j,dev in enumerate(plat.get_devices()):
+                            cl_env = get_or_create_opencl_env(platform_id=i, device_id=j, 
+                                    mpi_params=mpi_params, **kwds)
+                            cl_environments[None].append(cl_env)
+                else:
+                    cl_env = get_or_create_opencl_env(platform_id=__DEFAULT_PLATFORM_ID__, 
+                            device_id=__DEFAULT_DEVICE_ID__, 
+                            mpi_params=mpi_params, **kwds)
+                    cl_environments[None].append(cl_env)
+            else:
+                for cl_env in iter_clenv(cl_device_type=None, all_platforms=True):
+                    if (cl_env.device.type & cl_device_type):
+                        cl_environments[cl_device_type].append(cl_env)
+        
+        if len(cl_environments[cl_device_type])==0:
+            msg='     |Could not generate any opencl environment for device type {}.'
+            msg=msg.format(device_type)
+            if (cl_device_type == None):
+                raise RuntimeError(msg)
             else:
-                cl_env = get_or_create_opencl_env(mpi_params=mpi_params, **kwds)
-                cl_environments.append(cl_env)
-        if len(cl_environments)==0:
-            msg='Could not generate any opencl environment.'
-            raise RuntimeError(msg)
-        for cl_env in cl_environments:
+                print msg
+        for cl_env in cl_environments[cl_device_type]:
             yield cl_env
+            if not all_platforms:
+                return
 else:
     opencl_failed = pytest.mark.xfail
     iter_clenv = None
diff --git a/hysop/tools/contexts.py b/hysop/tools/contexts.py
index 2e77da7b74b94538e83309952359588d46265878..6ae0dbe6fc95b11160da54675598ef8ba4a2408f 100644
--- a/hysop/tools/contexts.py
+++ b/hysop/tools/contexts.py
@@ -20,7 +20,7 @@ def systrace(fn=None):
     yield 
     sys.settrace(__old_trace)
 
-class Timer:    
+class Timer(object):    
     def __enter__(self, factor=1):
         self.start = time.time()
         self.factor = factor
@@ -33,4 +33,3 @@ class Timer:
         self.interval = (self.end - self.start)*self.factor
         if exc_type:
             raise
-
diff --git a/hysop/tools/debug_dumper.py b/hysop/tools/debug_dumper.py
index a4ae068239aabbaccb5a809c75dbf0526e29e4f3..eebcbbefc5cd151391ec601c4cb71bb86a887a35 100644
--- a/hysop/tools/debug_dumper.py
+++ b/hysop/tools/debug_dumper.py
@@ -13,7 +13,7 @@ class DebugDumper(object):
         
         if os.path.exists(directory):
             if force_overwrite:
-                shutil.rmtree(path)
+                shutil.rmtree(directory)
             else:
                 msg='Directory \'{}\' already exists.'.format(directory)
                 raise RuntimeError(msg)
diff --git a/hysop/tools/enum.py b/hysop/tools/enum.py
index ece07b748d637df8a5afdef5fd2ed9daebcd5cd1..176ee92b2ae2add2827259fee15ba3f785c93a0d 100644
--- a/hysop/tools/enum.py
+++ b/hysop/tools/enum.py
@@ -216,9 +216,15 @@ class EnumFactory(object):
                 return self._field
             def value(self):
                 return self._value
+
             def __call__(self):
                 return self.value()
 
+            def __int__(self):
+                return int(self.value())
+            def __float__(self):
+                return float(self.value())
+
             def __str__(self):
                 return self.svalue()
             def __repr__(self):
@@ -232,6 +238,7 @@ class EnumFactory(object):
                 if not isinstance(other, self.__class__):
                     return NotImplemented
                 return self._value != other._value
+
             def __hash__(self):
                 return hash(self._field)
                 
@@ -278,3 +285,4 @@ if __name__ == '__main__':
     print repr(X), repr(Y), repr(Z)
     print
     print TestEnum.dtype, type(X.value())
+
diff --git a/hysop/tools/field_utils.py b/hysop/tools/field_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..2722b0bac971b0eaf8568f203a2f7c6e3506ef68
--- /dev/null
+++ b/hysop/tools/field_utils.py
@@ -0,0 +1,425 @@
+from hysop.tools.types import first_not_None, to_tuple
+from hysop.tools.sympy_utils import nabla, partial, subscript, subscripts, \
+                                    exponent, exponents, xsymbol, get_derivative_variables
+
+from sympy.printing.str import StrPrinter, StrReprPrinter
+from sympy.printing.ccode import C99CodePrinter
+from sympy.printing.latex import LatexPrinter
+
+class BasePrinter(object):
+    def print_Derivative(self, expr):
+        (bvar, pvar, vvar, lvar) = print_all_names(expr.args[0])
+        pvar = pvar.decode('utf-8')
+        all_xvars = get_derivative_variables(expr)
+        xvars   = tuple(set(all_xvars))
+        varpows = tuple(all_xvars.count(x) for x in xvars) 
+        bxvars  = tuple(print_name(x) for x in xvars)
+        pxvars  = tuple(print_pretty_name(x).decode('utf-8') for x in xvars)
+        vxvars  = tuple(print_var_name(x) for x in xvars)
+        lxvars  = tuple(print_latex_name(x) for x in xvars)
+        return DifferentialStringFormatter.format_pd(bvar, pvar, vvar, lvar,
+                                                     bxvars, pxvars, vxvars, lxvars,
+                                                     varpows=varpows)
+        
+    def _print(self, expr, **kwds):
+        try:
+            return super(BasePrinter, self)._print(expr, **kwds)
+        except:
+            print
+            msg='FATAL ERROR: {} failed to print expression {}.'
+            msg=msg.format(type(self).__name__, expr)
+            print msg
+            print
+            raise
+   
+
+class NamePrinter(BasePrinter, StrReprPrinter):
+    def _print(self, expr, **kwds):
+        if hasattr(expr, 'name'):
+            return expr.name
+        elif hasattr(expr, '_name'):
+            return expr._name
+        return super(NamePrinter, self)._print(expr, **kwds)
+    def _print_Derivative(self, expr):
+        return super(NamePrinter, self).print_Derivative(expr)[0]
+    def _print_Add(self, expr):
+        return super(NamePrinter, self)._print_Add(expr).replace(' ', '')
+    def _print_Mul(self, expr):
+        return super(NamePrinter, self)._print_Mul(expr).replace(' ', '')
+    def emptyPrinter(self, expr):
+        msg='\n{} does not implement _print_{}(self, expr).'
+        msg+='\nExpression is {}.'.format(expr)
+        msg+='\nExpression type MRO is:'
+        msg+='\n  *'+'\n  *'.join(t.__name__ for t in type(expr).__mro__)
+        msg=msg.format(self.__class__.__name__, expr.__class__.__name__)
+        raise NotImplementedError(msg)
+
+
+class PrettyNamePrinter(BasePrinter, StrPrinter):
+    def _print(self, expr, **kwds):
+        if hasattr(expr, 'pretty_name'):
+            return expr.pretty_name
+        elif hasattr(expr, '_pretty_name'):
+            return expr._pretty_name
+        return super(PrettyNamePrinter, self)._print(expr, **kwds)
+    def _print_Derivative(self, expr):
+        return super(PrettyNamePrinter, self).print_Derivative(expr)[1]
+    def emptyPrinter(self, expr):
+        msg='\n{} does not implement _print_{}(self, expr).'
+        msg+='\nExpression is {}.'.format(expr)
+        msg+='\nExpression type MRO is:'
+        msg+='\n  *'+'\n  *'.join(t.__name__ for t in type(expr).__mro__)
+        msg=msg.format(self.__class__.__name__, expr.__class__.__name__)
+        raise NotImplementedError(msg)
+
+
+class VarNamePrinter(BasePrinter, C99CodePrinter):
+    def _print(self, expr, **kwds):
+        if hasattr(expr, 'var_name'):
+            return expr.var_name
+        elif hasattr(expr, '_var_name'):
+            return expr._var_name
+        return super(VarNamePrinter, self)._print(expr, **kwds).replace(' ', '')
+    def _print_Derivative(self, expr):
+        return super(VarNamePrinter, self).print_Derivative(expr)[2]
+    def _print_Add(self, expr):
+        s = super(VarNamePrinter, self)._print_Add(expr)
+        s = s.replace(' + ', '_plus_').replace(' - ', '_minus_')
+        s = s.replace('+', 'plus_').replace('-', 'minus_')
+        return s
+    def _print_Mul(self, expr):
+        s = super(VarNamePrinter, self)._print_Mul(expr)
+        s = s.replace(' * ', '_times_').replace('+', 'plus_').replace('-', 'minus_')
+        return s
+    def emptyPrinter(self, expr):
+        msg='\n{} does not implement _print_{}(self, expr).'
+        msg+='\nExpression is {}.'.format(expr)
+        msg+='\nExpression type MRO is:'
+        msg+='\n  *'+'\n  *'.join(t.__name__ for t in type(expr).__mro__)
+        msg=msg.format(self.__class__.__name__, expr.__class__.__name__)
+        raise NotImplementedError(msg)
+
+
+class LatexNamePrinter(BasePrinter, LatexPrinter):
+    def _print(self, expr, **kwds):
+        if hasattr(expr, 'latex_name'):
+            return expr.latex_name
+        elif hasattr(expr, '_latex_name'):
+            return expr._latex_name
+        return super(LatexNamePrinter, self)._print(expr, **kwds)
+    def _print_Derivative(self, expr):
+        return super(LatexNamePrinter, self).print_Derivative(expr)[3]
+    def _print_int(self, expr):
+        return str(expr)
+    def emptyPrinter(self, expr):
+        msg='\n{} does not implement _print_{}(self, expr).'
+        msg+='\nExpression is {}.'.format(expr)
+        msg+='\nExpression type MRO is:'
+        msg+='\n  *'+'\n  *'.join(t.__name__ for t in type(expr).__mro__)
+        msg=msg.format(self.__class__.__name__, expr.__class__.__name__)
+        raise NotImplementedError(msg)
+
+pbn = NamePrinter()
+ppn = PrettyNamePrinter()
+#pvn = VarNamePrinter()
+pln = LatexNamePrinter()
+
+def print_name(expr):
+    return pbn.doprint(expr)
+
+def print_pretty_name(expr):
+    return ppn.doprint(expr)
+
+def print_var_name(expr):
+    return VarNamePrinter().doprint(expr)
+
+def print_latex_name(expr):
+    return pln.doprint(expr)
+
+def print_all_names(expr):
+    name = print_name(expr)
+    pretty_name = print_pretty_name(expr)
+    var_name = print_var_name(expr)
+    latex_name = print_latex_name(expr)
+    return (name, pretty_name, var_name, latex_name)
+
+
+def to_str(*args):
+    if len(args)==1:
+        args=to_tuple(args[0])
+    def _to_str(x):
+        if isinstance(x, unicode):
+            return x.encode('utf-8')
+        else:
+            return str(x)
+    return tuple(_to_str(y) for y in args)
+
+# exponents formatting functions
+bexp_fn = lambda x: '^{}'.format(x) if (x>1) else ''
+pexp_fn = lambda x, sep=',': exponents(x, sep=sep) if (x>1) else u''
+vexp_fn = lambda x: 'e{}'.format(x) if (x>1) else ''
+lexp_fn = lambda x: '^<LBRACKET>{}<RBRACKET>'.format(x) if (x>1) else ''
+
+# powers formatting functions
+bpow_fn = lambda x: '**{}'.format(x) if (x>1) else ''
+ppow_fn = lambda x, sep=',': exponents(x,sep=sep) if (x>1) else u''
+vpow_fn = lambda x: 'p{}'.format(x) if (x>1) else ''
+lpow_fn = lambda x: '^<LBRACKET>{}<RBRACKET>'.format(x) if (x>1) else ''
+
+# subcripts formatting functions
+bsub_fn = lambda x: '_{}'.format(x) if (x is not None) else ''
+psub_fn = lambda x, sep=',': subscripts(x,sep=sep) if (x is not None) else u''
+vsub_fn = lambda x: 's{}'.format(x) if (x is not None) else ''
+lsub_fn = lambda x: '_<LBRACKET>{}<RBRACKET>'.format(x) if (x is not None) else ''
+
+# components formatting functions
+bcomp_fn = lambda x: ','.join(to_str(x)) if (x is not None) else ''
+pcomp_fn = lambda x, sep=',': subscripts(x,sep=sep) if (x is not None) else u''
+vcomp_fn = lambda x: '_'+'_'.join(to_str(x)) if (x is not None) else ''
+lcomp_fn = lambda x: '_<LBRACKET>{}<RBRACKET>'.format(','.join(to_str(x))) if (x is not None) else ''
+
+# join formatting functions
+bjoin_fn = lambda x: '_'.join(to_str(x)) if (x is not None) else ''
+pjoin_fn = lambda x: ''.join(to_str(x)) if (x is not None) else u''
+vjoin_fn = lambda x: '_'.join(to_str(x)) if (x is not None) else ''
+ljoin_fn = lambda x: ''.join(to_str(x)) if (x is not None) else ''
+
+# divide formatting functions
+bdivide_fn = lambda x,y: '{}/{}'.format(x,y)
+pdivide_fn = lambda x,y: '{}/{}'.format(*to_str(x,y))
+vdivide_fn = lambda x,y: '{}__{}'.format(x,y)
+ldivide_fn = lambda x,y: '\dfrac<LBRACKET>{}<RBRACKET><LBRACKET>{}<RBRACKET>'.format(x,y)
+
+
+class DifferentialStringFormatter(object):
+    """
+    Utility class to format differential related strings like partial derivatives.
+
+    All string formatting function returns 4 different results:
+        *A string that can be used as identifier (name).
+        *A pretty string in utf-8 (pretty_name).
+        *A variable name that can be used as a valid C identifier for code generation (var_name).
+        *A latex string that can be compiled and displayed with latex (latex_name).
+    
+    Prefix used for methods:
+        b = name
+        p = pretty_name
+        v = var_name
+        l = latex_name
+
+    See __main__ at the bottom of this file for usage.
+    """
+
+    exp_fns    = (bexp_fn,  pexp_fn,  vexp_fn,  lexp_fn)
+    pow_fns    = (bpow_fn,  ppow_fn,  vpow_fn,  lpow_fn)
+    sub_fns    = (bsub_fn,  psub_fn,  vsub_fn,  lsub_fn)
+    comp_fns   = (bcomp_fn, pcomp_fn, vcomp_fn, lcomp_fn)
+    join_fns   = (bjoin_fn, pjoin_fn, vjoin_fn, ljoin_fn)
+    divide_fns = (bdivide_fn, pdivide_fn, vdivide_fn, ldivide_fn)
+
+    @staticmethod
+    def format_special_characters(ss):
+        special_characters = {
+            '<LBRACKET>': '{',
+            '<RBRACKET>': '}',
+        }
+        for (k,v) in special_characters.iteritems():
+            ss = ss.replace(k,v)
+        if isinstance(ss, unicode):
+            ss = ss.encode('utf-8')
+        return ss
+
+    @classmethod
+    def return_names(cls, *args, **kwds):
+        # fsc = format special characters
+        fsc=kwds.get('fsc', True)
+        assert len(args)>=1
+        if len(args)==1:
+            if fsc:
+                return args[0]
+            else:
+                cls.format_special_characters(args[0])
+        else:
+            if fsc:
+                return tuple(cls.format_special_characters(a) for a in args)
+            else:
+                return args
+
+    @classmethod
+    def format_partial_name(cls, bvar, pvar, vvar, lvar,
+                            bpow_fn=bpow_fn, ppow_fn=ppow_fn, vpow_fn=vpow_fn, lpow_fn=lpow_fn,    
+                            bcomp_fn=bcomp_fn, pcomp_fn=pcomp_fn, vcomp_fn=vcomp_fn, lcomp_fn=lcomp_fn,    
+                            blp='(', plp='', vlp='', llp='', 
+                            brp=')', prp='', vrp='',  lrp='', 
+                            bd='d', pd=partial, vd='d', ld='<LBRACKET>\partial<RBRACKET>', 
+                            dpow=1, varpow=1, components=None, 
+                            trigp=3, fsc=True):
+        assert (varpow != 0)
+        bd  = '' if (dpow==0) else bd
+        pd  = '' if (dpow==0) else pd
+        vd  = '' if (dpow==0) else vd
+        ld  = '' if (dpow==0) else ld
+        blp = '' if len(bvar) <= trigp else blp 
+        brp = '' if len(bvar) <= trigp else brp 
+        plp = '' if len(pvar) <= trigp else plp 
+        prp = '' if len(pvar) <= trigp else prp 
+        vlp = '' if len(vvar) <= trigp else vlp 
+        vrp = '' if len(vvar) <= trigp else vrp 
+        llp = '' if len(lvar) <= trigp else llp 
+        lrp = '' if len(lvar) <= trigp else lrp 
+        template=u'{d}{dpow}{lp}{var}{components}{rp}{varpow}' 
+        bname = template.format(d=bd, dpow=bpow_fn(dpow),
+                                components=bcomp_fn(components),
+                                var=bvar, varpow=bpow_fn(varpow),
+                                lp=blp, rp=brp)
+        pname = template.format(d=pd, dpow=ppow_fn(dpow),
+                                components=pcomp_fn(components),
+                                var=pvar, varpow=ppow_fn(varpow),
+                                lp=plp, rp=prp)
+        vname = template.format(d=vd, dpow=vpow_fn(dpow),
+                                components=vcomp_fn(components),
+                                var=vvar, varpow=vpow_fn(varpow),
+                                lp=vlp, rp=vrp)
+        lname = template.format(d=ld, dpow=lpow_fn(dpow),
+                                components=lcomp_fn(components),
+                                var=lvar, varpow=lpow_fn(varpow),
+                                lp=llp, rp=lrp)
+        return cls.return_names(bname, pname, vname, lname, fsc=fsc)
+
+    @classmethod
+    def format_partial_names(cls, bvars, pvars, vvars, lvars, varpows, 
+                            bjoin_fn=bjoin_fn, pjoin_fn=pjoin_fn, vjoin_fn=vjoin_fn, ljoin_fn=ljoin_fn, 
+                            components=None, fsc=True, **kwds):
+        bvars, pvars, vvars, lvars = to_tuple(bvars), to_tuple(pvars), to_tuple(vvars), to_tuple(lvars)
+        varpows = to_tuple(varpows)
+        assert len(bvars)==len(pvars)==len(vvars)==len(lvars)==len(varpows)
+        assert any(v>0 for v in varpows)
+        nvars = len(bvars)
+        if (components is not None):
+            components = to_tuple(components)
+            assert len(components)==nvars
+        else:
+            components = (None,)*nvars
+
+        bnames, pnames, vnames, lnames = (), (), (), ()
+        for (bvar, pvar, vvar, lvar, varpow, component) in \
+                zip(bvars, pvars, vvars, lvars, varpows, components):
+            if (varpow==0):
+                continue
+            res = cls.format_partial_name(bvar=bvar, pvar=pvar, vvar=vvar, lvar=lvar,
+                                          varpow=varpow, components=component, 
+                                          fsc=False, **kwds)
+            assert len(res)==4
+            bnames += (res[0],)
+            pnames += (res[1],)
+            vnames += (res[2],)
+            lnames += (res[3],)
+        return cls.return_names(bjoin_fn(bnames), pjoin_fn(pnames), 
+                                vjoin_fn(vnames), ljoin_fn(lnames), fsc=fsc)
+
+    @classmethod
+    def format_pd(cls, bvar, pvar, vvar, lvar,
+                       bxvars='x', pxvars=xsymbol, vxvars='x', lxvars='x', 
+                       varpows=1, var_components=None, xvars_components=None,
+                       bdivide_fn=bdivide_fn, pdivide_fn=pdivide_fn, vdivide_fn=vdivide_fn, ldivide_fn=ldivide_fn, 
+                       fsc=True, **kwds):
+
+        for k in ('dpow', 'components', 'bvars', 'pvars', 'vvars', 'lvars', 'varpow'):
+            assert k not in kwds, 'Cannot specify reserved keyword {}.'.format(k)
+
+        bxvars, pxvars, vxvars, lxvars = to_tuple(bxvars), to_tuple(pxvars), to_tuple(vxvars), to_tuple(lxvars)
+        varpows = to_tuple(varpows)
+        assert len(bxvars)==len(pxvars)==len(vxvars)==len(lxvars)==len(varpows)
+        assert any(v>0 for v in varpows)
+        dpow = sum(varpows)
+
+        numerator = cls.format_partial_name(bvar=bvar, pvar=pvar, 
+                                            vvar=vvar, lvar=lvar,
+                                            fsc=False, dpow=dpow,
+                                            components=var_components,
+                                            **kwds)
+
+        denominator = cls.format_partial_names(bvars=bxvars, pvars=pxvars,
+                                               vvars=vxvars, lvars=lxvars,
+                                               fsc=False, varpows=varpows,
+                                               components=xvars_components,
+                                               **kwds)
+
+        return cls.return_names(bdivide_fn(numerator[0], denominator[0]), 
+                                pdivide_fn(numerator[1], denominator[1]), 
+                                vdivide_fn(numerator[2], denominator[2]), 
+                                ldivide_fn(numerator[3], denominator[3]), fsc=fsc)
+        
+
+if __name__ == '__main__':
+    def _print(*args, **kwds):
+        if isinstance(args[0], tuple):
+            assert len(args)==1
+            args = args[0]
+        if ('multiline' in kwds) and (kwds['multiline'] is True):
+            for a in args:
+                print a
+        else:
+            print (u', '.join(a.decode('utf-8') for a in args)).encode('utf-8')
+
+    print
+    bvar, pvar, vvar, lvar = 'Fext', u'F\u1d49xt', 'Fext', '<LBRACKET>F_<LBRACKET>ext<RBRACKET><RBRACKET>'
+    _print(DifferentialStringFormatter.return_names(bvar, pvar, vvar, lvar))
+
+    print
+    _print(DifferentialStringFormatter.format_partial_name(bvar, pvar, vvar, lvar, dpow=0))
+    _print(DifferentialStringFormatter.format_partial_name(bvar, pvar, vvar, lvar, dpow=1))
+    _print(DifferentialStringFormatter.format_partial_name(bvar, pvar, vvar, lvar, dpow=2))
+    _print(DifferentialStringFormatter.format_partial_name(bvar, pvar, vvar, lvar, dpow=3, components=0))
+    _print(DifferentialStringFormatter.format_partial_name(bvar, pvar, vvar, lvar, dpow=4, components=(0,2)))
+    
+    print
+    bvar, pvar, vvar, lvar = ('x',)*4
+    _print(DifferentialStringFormatter.format_partial_name(bvar, pvar, vvar, lvar, varpow=1))
+    _print(DifferentialStringFormatter.format_partial_name(bvar, pvar, vvar, lvar, varpow=2))
+    _print(DifferentialStringFormatter.format_partial_name(bvar, pvar, vvar, lvar, varpow=3, components=0))
+    _print(DifferentialStringFormatter.format_partial_name(bvar, pvar, vvar, lvar, varpow=4, components=(0,2)))
+    
+    print
+    bvar, pvar, vvar, lvar = (('x','y'),)*4
+    try:
+        _print(DifferentialStringFormatter.format_partial_names(bvar, pvar, vvar, lvar, varpows=(0,0)))
+        raise RuntimeError()
+    except AssertionError:
+        pass
+    _print(DifferentialStringFormatter.format_partial_names(bvar, pvar, vvar, lvar, varpows=(0,1)))
+    _print(DifferentialStringFormatter.format_partial_names(bvar, pvar, vvar, lvar, varpows=(1,0)))
+    _print(DifferentialStringFormatter.format_partial_names(bvar, pvar, vvar, lvar, varpows=(1,1)))
+    _print(DifferentialStringFormatter.format_partial_names(bvar, pvar, vvar, lvar, varpows=(1,2)))
+    _print(DifferentialStringFormatter.format_partial_names(bvar, pvar, vvar, lvar, varpows=(2,2)))
+    _print(DifferentialStringFormatter.format_partial_names(bvar, pvar, vvar, lvar, varpows=(2,2), components=(0,1)))
+    _print(DifferentialStringFormatter.format_partial_names(bvar, pvar, vvar, lvar, varpows=(2,2), components=((0,1),(1,0))))
+
+    print
+    bvar, pvar, vvar, lvar = 'Fext', u'F\u1d49xt', 'Fext', '<LBRACKET>F_<LBRACKET>ext<RBRACKET><RBRACKET>'
+    bxvars, pxvars, vxvars, lxvars = (('x','y'),)*4
+    _print(DifferentialStringFormatter.format_pd(bvar, pvar, vvar, lvar))
+    _print(DifferentialStringFormatter.format_pd(bvar, pvar, vvar, lvar, varpows=2))
+    _print(DifferentialStringFormatter.format_pd(bvar, pvar, vvar, lvar, 
+                                              bxvars, pxvars, vxvars, lxvars,
+                                              varpows=(1,0)))
+    _print(DifferentialStringFormatter.format_pd(bvar, pvar, vvar, lvar, 
+                                              bxvars, pxvars, vxvars, lxvars,
+                                              varpows=(0,1)))
+    _print(DifferentialStringFormatter.format_pd(bvar, pvar, vvar, lvar, 
+                                              bxvars, pxvars, vxvars, lxvars,
+                                              varpows=(1,1)))
+    _print(DifferentialStringFormatter.format_pd(bvar, pvar, vvar, lvar, 
+                                              bxvars, pxvars, vxvars, lxvars,
+                                              varpows=(5,2)))
+    
+    print
+    bxvars, pxvars, vxvars, lxvars = (('x',)*5,)*4
+    varpows = (1,)*5
+    xvars_components = range(5)
+    var_components=(0,4,3,2)
+    _print(DifferentialStringFormatter.format_pd(bvar, pvar, vvar, lvar, 
+                                              bxvars, pxvars, vxvars, lxvars,
+                                              varpows=varpows, xvars_components=xvars_components,
+                                              var_components=var_components), multiline=True)
+
diff --git a/hysop/tools/interface.py b/hysop/tools/interface.py
new file mode 100644
index 0000000000000000000000000000000000000000..dcd1334ab0550d5c37fdd01b540812049136b94d
--- /dev/null
+++ b/hysop/tools/interface.py
@@ -0,0 +1,177 @@
+
+from abc import ABCMeta, abstractmethod
+from hysop.tools.types import check_instance, first_not_None, to_tuple
+from hysop.tools.numpywrappers import npw
+
+
+class SymbolContainerI(object):
+    __metaclass__ = ABCMeta
+    
+    def _get_symbol(self):
+        """
+        Return a Symbol that can be used to compute symbolic expressions
+        referring to this continuous field.
+        """
+        assert hasattr(self, '_symbol'), 'Symbol has not been defined.'
+        return self._symbol
+    
+    symbol = property(_get_symbol)
+    s = property(_get_symbol)
+
+
+class NamedObjectI(object):
+    __metaclass__ = ABCMeta
+    
+    def __new__(cls, name, pretty_name=None, latex_name=None, var_name=None, **kwds):
+        """
+        Create an abstract named object that contains a symbolic value.
+        name : string
+            A name for the field.
+        pretty_name: string or unicode, optional.
+            A pretty name used for display whenever possible (unicode supported).
+            Defaults to name.
+        kwds: dict
+            Keywords arguments for base class.
+        """
+        
+        obj = super(NamedObjectI, cls).__new__(cls, **kwds)
+        obj.rename(name=name, pretty_name=pretty_name, 
+                    latex_name=latex_name, var_name=var_name)
+        return obj
+    
+    def rename(self, name, pretty_name=None, latex_name=None, var_name=None):
+        """Change the names of this object."""
+        check_instance(name, str)
+        check_instance(pretty_name, (str,unicode), allow_none=True)
+        check_instance(latex_name, str, allow_none=True)
+        
+        pretty_name = first_not_None(pretty_name, name)
+        latex_name  = first_not_None(latex_name, name)
+        
+        if isinstance(pretty_name, unicode):
+            pretty_name = pretty_name.encode('utf-8')
+        check_instance(pretty_name, str)
+
+        self._name   = name
+        self._pretty_name = pretty_name
+        self._latex_name = latex_name
+        
+    def _get_name(self):
+        """Return the name of this field."""
+        return self._name
+    def _get_pretty_name(self):
+        """Return the pretty name of this field."""
+        return self._pretty_name
+    def _get_latex_name(self):
+        """Return the latex name of this field."""
+        return self._latex_name
+    
+    def __str__(self):
+        return self.long_description()
+    
+    @abstractmethod
+    def short_description(self):
+        """Short description of this field as a string."""
+        pass
+
+    @abstractmethod
+    def long_description(self):
+        """Long description of this field as a string."""
+        pass
+
+    name = property(_get_name)
+    pretty_name = property(_get_pretty_name)
+    latex_name = property(_get_latex_name)
+
+
+class NamedScalarContainerI(NamedObjectI, SymbolContainerI):
+    @property
+    def ndim(self):
+        """Number of dimensions of this this tensor."""
+        return 0
+    
+    def _get_var_name(self):
+        """Return the variable name of this field."""
+        return self._var_name
+    
+    def rename(self, name, pretty_name=None, 
+                latex_name=None, var_name=None):
+        """Change the names of this object."""
+        super(NamedScalarContainerI, self).rename(name=name, 
+                pretty_name=pretty_name, latex_name=latex_name)
+        self.check_and_set_varname(first_not_None(var_name, self._name))
+    
+    def check_and_set_varname(self, var_name):
+        check_instance(var_name, str, allow_none=True)
+
+        msg='Invalid variable name {}.'.format(var_name)
+        if var_name[0] in tuple(str(x) for x in range(10)):
+            raise RuntimeError(msg)
+        for c in '/*+-=|&()[]{}-!?:;,\'"#$^%<>@':
+            if c in var_name:
+                raise RuntimeError(msg)
+        self._var_name = var_name
+    
+    var_name = property(_get_var_name)
+
+
+class NamedTensorContainerI(NamedObjectI, SymbolContainerI):
+    def __new__(cls, contained_objects, **kwds):
+        check_instance(contained_objects, npw.ndarray)
+        obj = super(NamedTensorContainerI, cls).__new__(cls, **kwds)
+        obj._contained_objects = contained_objects
+        return obj
+    
+    def rename(self, name, pretty_name=None, 
+                latex_name=None, var_name=None):
+        """Change the names of this object."""
+        assert (var_name is None), 'Tensor do not have variable names.'
+        super(NamedTensorContainerI, self).rename(name=name, 
+                pretty_name=pretty_name, latex_name=latex_name)
+    
+    @property
+    def size(self):
+        """Full size of this container as if it was a 1D tensor."""
+        return self._contained_objects.size
+
+    @property
+    def shape(self):
+        """Shape of this tensor."""
+        return self._contained_objects.shape
+    
+    @property
+    def ndim(self):
+        """Number of dimensions of this this tensor."""
+        return self._contained_objects.ndim
+
+    def new_empty_array(self, dtype=object):
+        """Return a new empty array of the same shape as self."""
+        if (dtype is object):
+            array = npw.empty(shape=self.shape, dtype=dtype)
+            array[...] = None
+        else:
+            array = npw.zeros(shape=self.shape, dtype=dtype)
+        return array
+
+    def iter_fields(self):
+        """Return an iterator on unique scalar object along with 1d index."""
+        for (i,obj) in enumerate(self._contained_objects.ravel()):
+            yield (i,obj)
+
+    def nd_iter(self):
+        """Return an nd-indexed iterator of contained objects."""
+        for idx in npw.ndindex(*self._contained_objects.shape):
+            yield (idx, self._contained_objects[idx])
+    
+    def __iter__(self):
+        """Return an iterator on unique scalar objects."""
+        return self._contained_objects.ravel().__iter__()
+
+    def __contains__(self, obj):
+        """Check if a scalar object is contained in self."""
+        return obj in self._contained_objects
+    
+    @abstractmethod
+    def __getitem__(self, slc):
+        pass
+
diff --git a/hysop/tools/io_utils.py b/hysop/tools/io_utils.py
index ce02db2fe9c9202422b27bfbc73dfa8683e0aeb9..997cf35d02333a3d33cbc04cf8014524186028d3 100755
--- a/hysop/tools/io_utils.py
+++ b/hysop/tools/io_utils.py
@@ -8,7 +8,7 @@
 * :class:`~XMF`, tools to prepare/write xmf files.
 
 """
-import os, h5py, psutil, warnings, tempfile
+import os, h5py, psutil, warnings, tempfile, socket
 import subprocess32 as subprocess
 from collections import namedtuple
 from inspect import getouterframes, currentframe
@@ -134,14 +134,15 @@ class IO(object):
     @classmethod
     def set_cache_path(cls, path):
         if cls.is_shared_fs(path):
+            new_path += '/{}'.format(socket.gethostname())
             msg='\nSpecified cache path \'{}\' is stored on a network filesystem '  
             msg += 'which does not correctly support file locking.'
-            msg += '\nReverting cache_path to \'{}\'.'
-            msg=msg.format(path, self._cache_path)
+            msg += '\nSetting cache_path to \'{}\'.'
+            msg=msg.format(path, new_path)
             warnings.warn(msg, HysopWarning)
-        else:
-            IO._cache_path = path
-            IO.check_dir(path)
+            path = new_path
+        IO._cache_path = path
+        IO.check_dir(path)
 
     @staticmethod
     def set_datasetname(field_name, topo, direction=None):
diff --git a/hysop/tools/misc.py b/hysop/tools/misc.py
index af5575a15a32eb43864c2a00ac3370c2d0aa48c2..9875e9ab6b1e80f8877ce25fa8223afd8cab9aff 100644
--- a/hysop/tools/misc.py
+++ b/hysop/tools/misc.py
@@ -12,13 +12,18 @@ from hysop.constants import HYSOP_REAL, HYSOP_INTEGER
 
 def prod(values):
     """
-    Like sum but for products.
+    Like sum but for products (of integers).
     """
     try:
         return np.prod(values, dtype=np.int64)
     except:
         return np.prod(values)
 
+def compute_nbytes(shape, dtype):
+    nbytes = prod(shape) * dtype.itemsize
+    assert nbytes>0
+    return nbytes
+
 def get_default_args(func):
     """
     returns a dictionary of arg_name:default_values for the input function.
diff --git a/hysop/tools/numba_utils.py b/hysop/tools/numba_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8abd9cd42348f6b4f83986057856b1b77a4f886
--- /dev/null
+++ b/hysop/tools/numba_utils.py
@@ -0,0 +1,95 @@
+
+import numba as nb
+import numpy as np
+from hysop.core.arrays.array import Array
+
+def make_numba_signature(*args, **kwds):
+    raise_on_cl_array = kwds.pop('raise_on_cl_array', True)
+    if kwds:
+        msg='Unknown kwds {}.'.forma(kwds.keys())
+        raise RuntimeError(kwds)
+    dtype_to_ntype = {
+            int:        np.int32,
+            long:       np.int64,
+            float:      np.float64,
+
+            np.int8:    nb.int8,
+            np.int16:   nb.int16,
+            np.int32:   nb.int32,
+            np.int64:   nb.int64,
+
+            np.uint8:   nb.uint8,
+            np.uint16:  nb.uint16,
+            np.uint32:  nb.uint32,
+            np.uint64:  nb.uint64,
+
+            np.float32: nb.float32,
+            np.float64: nb.float64,
+
+            np.complex64:  nb.complex64,
+            np.complex128: nb.complex128,
+    }
+    
+    sizes = ('m','n','p','q','r','s') + tuple('n{}'.format(i) for i in xrange(10))
+    registered_sizes = {}
+    def format_shape(*shape):
+        res = '('
+        for (i,s) in enumerate(shape):
+            if s in registered_sizes:
+                sc = registered_sizes[s]
+            else:
+                sc = sizes[len(registered_sizes)]
+                registered_sizes[s] = sc
+            res += sc
+            if (i!=len(shape)-1):
+                res+=','
+        res += ')'
+        return res
+    
+    numba_args = ()
+    numba_layout = ()
+    for i,a in enumerate(args):
+        from hysop.backend.device.opencl import clArray
+        if isinstance(a, Array):
+            a = a.handle
+        if isinstance(a, clArray.Array):
+            # some opencl arrays can be mapped to host
+            if raise_on_cl_array:
+                msg='Numba signature: Got a cl.Array or hysop.OpenClArray for argument {} (shape={}, dtype={}).'
+                msg=msg.format(i, a.shape, a.dtype)
+                raise ValueError(msg)
+            assert a.dtype.type in dtype_to_ntype, a.dtype.type
+            dtype = dtype_to_ntype[a.dtype.type]
+            if a.flags.c_contiguous:
+                na = nb.types.Array(dtype=dtype, ndim=a.ndim, layout='C')
+            elif a.flags.f_contiguous:
+                na = nb.types.Array(dtype=dtype, ndim=a.ndim, layout='F')
+            else:
+                na = nb.types.Array(dtype=dtype, ndim=a.ndim, layout='A')
+            numba_layout += (format_shape(*a.shape),)
+        elif isinstance(a, np.ndarray):
+            assert a.dtype.type in dtype_to_ntype, a.dtype.type
+            dtype = dtype_to_ntype[a.dtype.type]
+            if a.flags['C_CONTIGUOUS']:
+                na = nb.types.Array(dtype=dtype, ndim=a.ndim, layout='C')
+            elif a.flags['F_CONTIGUOUS']:
+                na = nb.types.Array(dtype=dtype, ndim=a.ndim, layout='F')
+            else:
+                na = nb.types.Array(dtype=dtype, ndim=a.ndim, layout='A')
+            numba_layout += (format_shape(*a.shape),)
+        elif isinstance(a, np.dtype):
+            na = dtype_to_ntype[a.type]
+            numba_layout += ('()',)
+        elif isinstance(a, type):
+            na = dtype_to_ntype[a]
+            numba_layout += ('()',)
+        elif type(a) in dtype_to_ntype:
+            na = dtype_to_ntype[type(a)]
+            numba_layout += ('()',)
+        else:
+            msg='Uknown argument type {}.'.format(type(a).__mro__)
+            raise NotImplementedError(msg)
+        
+        numba_args += (na,)
+
+    return nb.void(*numba_args), ','.join(numba_layout)
diff --git a/hysop/tools/numerics.py b/hysop/tools/numerics.py
index 877c39232ab7256d200faf415b9e17f799b6a880..f4dcd832c8cef203761f0c2495f906da4b2ff0e1 100644
--- a/hysop/tools/numerics.py
+++ b/hysop/tools/numerics.py
@@ -167,6 +167,19 @@ def float_to_complex_dtype(dtype):
         msg=msg.format(dtype)
         raise RuntimeError(msg)
 
+def determine_fp_types(dtype):
+    if is_fp(dtype):
+        ftype = dtype
+        ctype = float_to_complex_dtype(ftype)
+    elif is_complex(dtype):
+        ctype = dtype
+        ftype = complex_to_float_dtype(ctype)
+    else:
+        msg='{} is not a floating point or complex data type.'
+        msg=msg.format(dtype)
+        raise ValueError(msg)
+    return (np.dtype(ftype), np.dtype(ctype))
+
 def find_common_dtype(*args):
     dtypes   = tuple(get_dtype(arg) for arg in args)
     itemsize = tuple(get_itemsize(x) for x in dtypes)
diff --git a/hysop/tools/numpywrappers.py b/hysop/tools/numpywrappers.py
index 16da85632c10f631441b168b81bb10c4623d7253..9b5a528ce21ad28bbdd6bcb05ccc5db432c37cff 100644
--- a/hysop/tools/numpywrappers.py
+++ b/hysop/tools/numpywrappers.py
@@ -130,9 +130,9 @@ def slices_empty(slices, shape):
     slices = (slices,) if isinstance(slices,slice) else slices
     assert len(shape) >= len(slices)
     shape = shape[:len(slices)]
-    empty = (slices[i].indices(shape[i]) for i in xrange(len(slices)) \
-            if isinstance(slices[i], slice) )
-    empty = ( i>=j for (i,j,_) in empty )
+    empty = tuple( slices[i].indices(shape[i]) for i in xrange(len(slices)) \
+                   if isinstance(slices[i], slice) )
+    empty = tuple( (i>=j) for (i,j,_),ss in zip(empty,shape) )
     return any(empty)
 
 def set_readonly(*args):
diff --git a/hysop/tools/parameters.py b/hysop/tools/parameters.py
index 4b53385b1889f7f2aa5d1bb738ef016fb7e50e06..c00400c3bd3e30fd44423a7d204a75d97a2684c6 100755
--- a/hysop/tools/parameters.py
+++ b/hysop/tools/parameters.py
@@ -3,15 +3,18 @@
 .. currentmodule hysop.tools
 
 * :class:`~MPIParams`
-* :class:`~Discretization`
+* :class:`~CartesianDiscretization`
 
 """
 
+import hashlib
 from collections import namedtuple
-from hysop.deps import hashlib
+
+from hysop.constants import HYSOP_DEFAULT_TASK_ID, BoundaryCondition
+from hysop.tools.types import first_not_None, check_instance
 from hysop.tools.hash import hash_communicator
+from hysop.tools.numpywrappers import npw
 from hysop.core.mpi import main_comm, main_rank, MPI
-from hysop.constants import HYSOP_DEFAULT_TASK_ID
 
 class MPIParams(namedtuple('MPIParams', ['comm', 'size', 'task_id',
                                          'rank', 'on_task'])):
@@ -67,17 +70,26 @@ class MPIParams(namedtuple('MPIParams', ['comm', 'size', 'task_id',
         h.update(str(self.on_task))
         return hash(h.hexdigest()) ^ id(self.comm)
 
-class Discretization(namedtuple("Discretization", ['resolution', 'ghosts'])):
+
+class CartesianDiscretization(namedtuple("CartesianDiscretization", 
+    ['resolution', 'ghosts', 'lboundaries', 'rboundaries'])):
     """
     A struct to handle discretization parameters:
     - a resolution (either a list of int or a numpy array of int)
+      resolution is GRID_RESOLUTION. GLOBAL_RESOLUTION is GRID_RESOLUTION + PERIODICITY.
     - number of points in the ghost-layer. One value per direction, list
-    or array. Default = None.
+        or array. Default = None (ie. no ghosts).
+    - global boundary conditions that should be prescribed on the left and the
+        right of the box shaped domain for each axis. Defaults to periodic 
+        boundary conditions everywhere.
     """
-    def __new__(cls, resolution, ghosts=None):
+    def __new__(cls, resolution, ghosts=None, 
+                    lboundaries=None, rboundaries=None,
+                    default_boundaries=False):
+        assert not ((lboundaries is None) ^ (rboundaries is None))
         from hysop.tools.numpywrappers import npw
         resolution = npw.asdimarray(resolution)
-        if ghosts is not None:
+        if (ghosts is not None):
             ghosts = npw.asintegerarray(ghosts)
             msg = 'Dimensions of resolution and ghosts parameters'
             msg += ' are not complient.'
@@ -85,13 +97,68 @@ class Discretization(namedtuple("Discretization", ['resolution', 'ghosts'])):
             assert all(ghosts >= 0)
         else:
             ghosts = npw.integer_zeros(resolution.size)
-        return super(Discretization, cls).__new__(cls, resolution, ghosts)
+
+        assert not ((lboundaries is None) ^ (rboundaries is None))
+
+        if default_boundaries:
+            assert (lboundaries is None)
+            assert (rboundaries is None)
+            lboundaries = npw.empty(shape=(resolution.size,), dtype=object)
+            lboundaries[...] = BoundaryCondition.PERIODIC
+            rboundaries = lboundaries.copy()
+
+        check_instance(lboundaries, npw.ndarray, dtype=object, 
+                size=resolution.size, values=BoundaryCondition,
+                allow_none=True)
+        check_instance(rboundaries, npw.ndarray, dtype=object, 
+                size=resolution.size, values=BoundaryCondition, 
+                allow_none=True)
+
+        npw.set_readonly(resolution, ghosts)
+        if (lboundaries is not None):
+            npw.set_readonly(lboundaries, rboundaries)
+
+        return super(CartesianDiscretization, cls).__new__(cls, resolution, ghosts,
+                lboundaries, rboundaries)
+    
+    @property
+    def boundaries(self):
+        """Left and right boundary conditions as a tuple."""
+        if (self.lboundaries is None):
+            raise AttributeError
+        else:
+            return (self.lboundaries, self.rboundaries)
+        
+    @property
+    def periodicity(self):
+        if (self.lboundaries is None) or (self.rboundaries is None):
+            raise AttributeError
+        else:
+            return (self.lboundaries == BoundaryCondition.PERIODIC)
+
+    @property
+    def grid_resolution(self):
+        """Effective grid resolution given by user."""
+        return self.resolution
+
+    @property
+    def global_resolution(self):
+        """
+        Logical grid resolution (grid_resolution + periodicity).
+        Can only be fetched if boundaries have been specified.
+        """
+        return self.grid_resolution + self.periodicity
 
     def __eq__(self, other):
         if self.__class__ != other.__class__:
             return NotImplemented
-        return (self.resolution == other.resolution).all() and\
-            (self.ghosts == other.ghosts).all()
+        if (self.lboundaries is None) ^ (other.lboundaries is None):
+            return False
+        match  = (self.resolution == other.resolution).all()
+        match &= (self.ghosts == other.ghosts).all()
+        match &= (self.lboundaries == other.lboundaries).all()
+        match &= (self.rboundaries == other.rboundaries).all()
+        return match
 
     def __ne__(self, other):
         result = self.__eq__(other)
@@ -100,14 +167,23 @@ class Discretization(namedtuple("Discretization", ['resolution', 'ghosts'])):
         return not result
 
     def __str__(self):
-        s = 'discretization:'
-        s+= '\n  *resolution: {}'.format(self.resolution)
-        s+= '\n  *ghosts: {}'.format(self.ghosts)
+        s = 'Cartesian discretization:'
+        s+= '\n  *resolution:  {}'.format(self.resolution)
+        s+= '\n  *ghosts:      {}'.format(self.ghosts)
+        if self.lboundaries:
+            s+= '\n  *lboundaries: {}'.format(self.lboundaries.tolist())
+            s+= '\n  *rboundaries: {}'.format(self.rboundaries.tolist())
+        else:
+            s+= '\n  *lboundaries: None'
+            s+= '\n  *rboundaries: None'
         return s
 
     def __hash__(self):
-        from hysop.deps import hashlib, np
         h = hashlib.sha1()
-        h.update(self.resolution.view(np.uint8))
-        h.update(self.ghosts.view(np.uint8))
+        h.update(self.resolution.view(npw.uint8))
+        h.update(self.ghosts.view(npw.uint8))
+        if (self.lboundaries is not None):
+            h.update(str(hash(tuple(int(bd) for bd in self.lboundaries))))
+        if (self.rboundaries is not None):
+            h.update(str(hash(tuple(int(bd) for bd in self.rboundaries))))
         return hash(h.hexdigest())
diff --git a/hysop/tools/spectral_utils.py b/hysop/tools/spectral_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f70d00ee508ef707f9113fff288cf679ea9bb3a
--- /dev/null
+++ b/hysop/tools/spectral_utils.py
@@ -0,0 +1,726 @@
+
+import numpy as np
+import sympy as sm
+
+from hysop.tools.types import check_instance, first_not_None, to_tuple
+from hysop.tools.numerics import is_fp, is_complex, complex_to_float_dtype, float_to_complex_dtype
+from hysop.tools.sympy_utils import Expr, Symbol, Dummy, subscript, tensor_symbol
+from hysop.constants import BoundaryCondition, BoundaryExtension, TransformType
+from hysop.fields.continuous_field import Field, ScalarField, TensorField
+
+
+class SpectralTransformUtils(object):
+    """Class that contains userfull methods for SpectralTransform setup."""
+
+    cosine_transforms = (
+        TransformType.DCT_I,  TransformType.DCT_II,  TransformType.DCT_III,  TransformType.DCT_IV,
+        TransformType.IDCT_I, TransformType.IDCT_II, TransformType.IDCT_III, TransformType.IDCT_IV,
+    )
+    sine_transforms = (
+        TransformType.DST_I,  TransformType.DST_II,  TransformType.DST_III,  TransformType.DST_IV,
+        TransformType.IDST_I, TransformType.IDST_II, TransformType.IDST_III, TransformType.IDST_IV,
+    )
+
+    R2R_transforms = cosine_transforms + sine_transforms
+    R2C_transforms = (TransformType.RFFT,)
+    C2R_transforms = (TransformType.IRFFT,)
+    C2C_transforms = (TransformType.FFT, TransformType.IFFT)
+
+    forward_transforms = (
+      TransformType.FFT, TransformType.RFFT,
+      TransformType.DST_I, TransformType.DST_II, TransformType.DST_III, TransformType.DST_IV,
+      TransformType.DCT_I, TransformType.DCT_II, TransformType.DCT_III, TransformType.DCT_IV,
+    )
+    
+    backward_transforms = (
+      TransformType.IFFT, TransformType.IRFFT,
+      TransformType.IDST_I, TransformType.IDST_II, TransformType.IDST_III, TransformType.IDST_IV,
+      TransformType.IDCT_I, TransformType.IDCT_II, TransformType.IDCT_III, TransformType.IDCT_IV,
+    )
+
+    @classmethod
+    def is_cosine(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform in cls.cosine_transforms)
+    @classmethod
+    def is_sine(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform in cls.sine_transforms)
+    @classmethod
+    def is_R2C(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform in cls.R2C_transforms)
+    @classmethod
+    def is_R2R(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform in cls.R2R_transforms)
+    @classmethod
+    def is_R2C(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform in cls.R2C_transforms)
+    @classmethod
+    def is_C2R(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform in cls.C2R_transforms)
+    @classmethod
+    def is_C2C(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform in cls.C2C_transforms)
+    @classmethod
+    def is_forward(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform in cls.forward_transforms)
+    @classmethod
+    def is_backward(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform in cls.backward_transforms)
+    @classmethod
+    def is_none(cls, transform):
+        check_instance(transform, TransformType)
+        return (transform is TransformType.NONE)
+
+    @classmethod
+    def get_transform_offsets(cls, transform):
+        """Return left and right transform offsets."""
+        check_instance(transform, TransformType)
+        if cls.is_R2R(transform):
+            if (transform is TransformType.DST_I):
+                return (1,1)
+            elif (transform is TransformType.DST_III):
+                return (1,0)
+            elif (transform is TransformType.DCT_III):
+                return (0,1)
+            elif (transform is TransformType.DCT_I):
+                return (0,0)
+            else:
+                msg='Unknown real to real forward transform {}.'.format(transform)
+                raise ValueError(msg)
+        else:
+            return (0,0)
+
+    @classmethod
+    def get_transform_resolution(cls, resolution, *transforms):
+        resolution = to_tuple(resolution, cast=int)
+        check_instance(transforms, tuple, values=TransformType, size=len(resolution))
+        dim = len(resolution)
+        shape = []
+        transform_offsets  = []
+        for i,(tr,si) in enumerate(zip(transforms[::-1], resolution)):
+            (lo,ro) = cls.get_transform_offsets(tr)
+            shape.append(si-lo-ro)
+            transform_offsets.append((lo,ro))
+        return tuple(shape), tuple(transform_offsets)
+
+        
+    @classmethod
+    def compute_wave_numbers(cls, transform, N, L, ftype):
+        """Compute wave numbers of a given transform."""
+        check_instance(transform, TransformType)
+        check_instance(N, int)
+        check_instance(L, float)
+        assert is_fp(ftype)
+        otype = ftype
+        if (transform is TransformType.FFT):
+            freqs = 2.0*np.pi*1j*np.fft.fftfreq(n=N, d=L/N)
+            otype = float_to_complex_dtype(ftype)
+        elif (transform is TransformType.RFFT):
+            freqs = 2.0*np.pi*1j*np.fft.rfftfreq(n=N, d=L/N)
+            otype = float_to_complex_dtype(ftype)
+        elif (transform in (TransformType.DCT_I, TransformType.DST_I)):
+            freqs = np.pi*(np.arange(N, dtype=ftype)+0.0)/L
+        elif (transform in (TransformType.DCT_III, TransformType.DST_III)):
+            N -= 1
+            freqs = np.pi*(np.arange(N, dtype=ftype)+0.5)/L
+        else:
+            msg='Unknown transform type {}.'.format(transform)
+            raise ValueError(msg)
+        freqs = freqs.astype(otype, copy=True)
+        return freqs
+
+    @classmethod
+    def determine_output_dtype(cls, input_dtype, *transforms):
+        """Compute output data type from input data type and list of forward transforms."""
+        dtype = input_dtype
+        for tr in transforms:
+            if cls.is_backward(tr):
+                msg='{} is not a forward transform.'
+                msg=msg.format(tr)
+                raise ValueError(msg)
+            elif cls.is_none(tr):
+                continue
+            elif cls.is_R2R(tr):
+                msg='Expected a floating point data type but got {}.'.format(dtype)
+                assert is_fp(dtype), msg
+                # data type does not change
+            elif cls.is_R2C(tr):
+                msg='Expected a floating point data type but got {}.'.format(dtype)
+                assert is_fp(dtype), msg
+                dtype = float_to_complex_dtype(dtype)
+            elif cls.is_C2R(tr):
+                msg='Expected a complex data type but got {}.'.format(dtype)
+                assert is_complex(dtype), msg
+                dtype = complex_to_float_dtype(dtype)
+            elif cls.is_C2C(tr):
+                msg='Expected a complex data type but got {}.'.format(dtype)
+                assert is_complex(dtype), msg
+                # data type does not change
+            else:
+                msg='Unknown transform type {}.'.format(tr)
+                raise ValueError(msg)
+        return np.dtype(dtype)
+            
+    @classmethod
+    def determine_input_dtype(cls, output_dtype, *transforms):
+        """Compute input data type from output data type and list of backward transforms."""
+        backward_transforms = cls.get_inverse_transforms(*transforms)
+        return cls.determine_output_dtype(output_dtype, *backward_transforms)
+    
+    @classmethod
+    def parse_expression(cls, expr, replace_pows=True):
+        """
+        Extract all wave_numbers from expression.
+        If replace_pow is set, all wave_numbers powers will have their own symbol
+        and are replace in expression (this allows to precompute wavenumber powers).
+
+        Returns parsed expression and a set of spectral transforms and 
+        a set of contained wave_numbers.
+        """
+        from hysop.symbolic.spectral import WaveNumber, AppliedSpectralTransform
+        wave_numbers = set()
+        transforms = set()
+        def _extract(expr):
+            if isinstance(expr, WaveNumber):
+                wave_numbers.add(expr)
+                return expr
+            elif isinstance(expr, AppliedSpectralTransform):
+                transforms.add(expr)
+                return expr
+            elif replace_pows and \
+                 isinstance(expr, sm.Pow) and \
+                 isinstance(expr.args[0], WaveNumber) and \
+                 isinstance(expr.args[1], (int,long,np.integer,sm.Integer)):
+                wn = expr.args[0].pow(int(expr.args[1]))
+                wave_numbers.add(wn)
+                return wn
+            elif isinstance(expr, (sm.Symbol, sm.Number)):
+                return expr
+            elif isinstance(expr, sm.Expr):
+                args = ()
+                for a in expr.args:
+                    args += (_extract(a),)
+                try:
+                    return expr.func(*args)
+                except TypeError:
+                    msg='\nFATAL ERROR: Failed to rebuild expr {}'.format(expr)
+                    msg+='\n type is {}'.format(expr.func)
+                    msg+='\n'
+                    print msg
+                    raise
+            else:
+                return expr
+        expr = _extract(expr)
+        return (expr, transforms, wave_numbers)
+        
+
+    @classmethod
+    def generate_wave_number(cls, transform, axis, exponent):
+        """Create a new wavenumber. WaveNumbers are registered dummy symbols."""
+        from hysop.symbolic.spectral import WaveNumber
+        return WaveNumber(transform=transform, axis=axis, exponent=exponent)
+
+    @classmethod
+    def generate_wave_numbers(cls, *transforms):
+        """
+        Generare a list of wave_numbers in transform order.
+        Axis will match transform position.
+        """
+        wave_numbers = ()
+        for (i,tr)in enumerate(transforms):
+            wave_numbers += (cls.generate_wave_number(tr,i,1),)
+        return wave_numbers
+
+    @classmethod
+    def transforms_from_field(cls, field, transformed_axes):
+        """
+        Create a tuple of transforms by extracting field boundary conditions.
+        Note that transforms are returned in natural ordering (ie. contiguous X-axis last).
+        """
+        check_instance(field, ScalarField)
+        boundaries = tuple((lbd, rbd) for (lbd, rbd) 
+                            in zip(field.lboundaries, field.rboundaries))
+        transforms = cls.boundaries_to_transforms(boundaries[::-1], transformed_axes)[::-1]
+        return transforms
+
+    @classmethod
+    def boundaries_to_transforms(cls, boundaries, transformed_axes):
+        """
+        Return a tuple of TransformType from a tuple of (left_boundaries, right_boundaries).
+        """
+        check_instance(boundaries, tuple, values=tuple)
+        extensions = cls.boundaries_to_extensions(boundaries)
+        transforms = cls.extensions_to_transforms(extensions, transformed_axes)
+        return transforms
+
+    @classmethod
+    def boundaries_to_extensions(cls, boundaries):
+        """Convert a BoundaryCondition pair tuple to a BoundaryExtension pair tuple."""
+        check_instance(boundaries, tuple, values=tuple)
+        valid_boundary_pairs = (
+            (BoundaryCondition.PERIODIC,              BoundaryCondition.PERIODIC),
+            (BoundaryCondition.HOMOGENEOUS_DIRICHLET, BoundaryCondition.HOMOGENEOUS_DIRICHLET),
+            (BoundaryCondition.HOMOGENEOUS_NEUMANN,   BoundaryCondition.HOMOGENEOUS_DIRICHLET),
+            (BoundaryCondition.HOMOGENEOUS_DIRICHLET, BoundaryCondition.HOMOGENEOUS_NEUMANN),
+            (BoundaryCondition.HOMOGENEOUS_NEUMANN,   BoundaryCondition.HOMOGENEOUS_NEUMANN),
+        )
+        extensions = ()
+        for boundary_pair in boundaries:
+            if (boundary_pair not in valid_boundary_pairs):
+                msg='Invalid boundary pair {}, valid ones are\n  *{}'
+                msg=msg.format(boundary_pair, '\n  *'.join(str(vbp) 
+                    for vbp in valid_boundary_pairs))
+                raise ValueError(msg)
+            (left_bd, right_bd) = boundary_pair
+            left_ext  = cls.boundary_to_extension(left_bd)
+            right_ext = cls.boundary_to_extension(right_bd)
+            extension_pair = (left_ext, right_ext)
+            extensions += (extension_pair,)
+        return extensions
+
+    @classmethod
+    def boundary_to_extension(cls, boundary):
+        """Convert a BoundaryCondition to a BoundaryExtension"""
+        check_instance(boundary, BoundaryCondition)
+        if (boundary is BoundaryCondition.PERIODIC):
+            return BoundaryExtension.PERIODIC
+        elif (boundary is BoundaryCondition.HOMOGENEOUS_NEUMANN):
+            return BoundaryExtension.EVEN
+        elif (boundary is BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+            return BoundaryExtension.ODD
+        else:
+            msg='Unknown boundary condition {}.'.format(boundary)
+            raise NotImplementedError(msg)
+
+
+    @classmethod
+    def extensions_to_transforms(cls, extensions, transformed_axes, is_complex=False):
+        """Convert a BoundaryExtension pair tuple to a TransformType tuple."""
+        dim = len(extensions)
+        transforms = ()
+        for i,extension_pair in enumerate(extensions):
+            axis = dim-1-i
+            if (axis in transformed_axes):
+                transform = cls.extension_to_transform(*extension_pair, is_complex=is_complex)
+                if is_complex and cls.is_R2R(transform):
+                    raise ValueError('Data is complex but you try to apply a real to real transform.')
+                is_complex |= (transform is TransformType.RFFT)
+            else:
+                transform = TransformType.NONE
+            transforms += (transform,)
+        return transforms
+
+    @classmethod
+    def extension_to_transform(cls, left_ext, right_ext, is_complex=False):
+        """Convert a BoundaryExtension pair to a TransformType."""
+        check_instance(left_ext, BoundaryExtension)
+        check_instance(right_ext, BoundaryExtension)
+
+        valid_extension_pairs = (
+            (BoundaryExtension.PERIODIC,  BoundaryExtension.PERIODIC),
+            (BoundaryExtension.ODD,       BoundaryExtension.ODD),
+            (BoundaryExtension.EVEN,      BoundaryExtension.ODD),
+            (BoundaryExtension.ODD,       BoundaryExtension.EVEN),
+            (BoundaryExtension.EVEN,      BoundaryExtension.EVEN),
+        )
+        extension_pair = (left_ext, right_ext)
+        msg='Invalid domain extension pair {}, valid ones are\n  *{}'
+        msg=msg.format(extension_pair, '\n  *'.join(str(vep) 
+            for vep in valid_extension_pairs))
+
+        if (extension_pair not in valid_extension_pairs):
+                raise ValueError(msg)
+
+        if (left_ext is BoundaryExtension.PERIODIC):
+            if (right_ext is not BoundaryExtension.PERIODIC):
+                raise ValueError(msg)
+            if is_complex:
+                return TransformType.FFT
+            else:
+                return TransformType.RFFT
+        elif (left_ext is BoundaryExtension.EVEN):
+            if (right_ext is BoundaryExtension.EVEN):
+                return TransformType.DCT_I
+            elif (right_ext is BoundaryExtension.ODD):
+                return TransformType.DCT_III
+            else:
+                raise ValueError(msg)
+        elif (left_ext is BoundaryExtension.ODD):
+            if (right_ext is BoundaryExtension.EVEN):
+                return TransformType.DST_III
+            elif (left_ext is BoundaryExtension.ODD):
+                return TransformType.DST_I
+            else:
+                raise ValueError(msg)
+        else:
+            raise ValueError(msg)
+
+    @classmethod
+    def get_inverse_transforms(cls, *transforms):
+        """Get the inverse TransformType of a TransformType (for all input arguments)."""
+        known_inverse_transforms = {
+            TransformType.NONE:     TransformType.NONE,
+
+            TransformType.FFT:      TransformType.IFFT,
+            TransformType.RFFT:     TransformType.IRFFT,
+            TransformType.DCT_I:    TransformType.IDCT_I,
+            TransformType.DCT_II:   TransformType.IDCT_II,
+            TransformType.DCT_III:  TransformType.IDCT_III,
+            TransformType.DCT_IV:   TransformType.IDCT_IV,
+            TransformType.DST_I:    TransformType.IDST_I,
+            TransformType.DST_II:   TransformType.IDST_II,
+            TransformType.DST_III:  TransformType.IDST_III,
+            TransformType.DST_IV:   TransformType.IDST_IV,
+
+            TransformType.IFFT:     TransformType.FFT,
+            TransformType.IRFFT:    TransformType.RFFT,
+            TransformType.IDCT_I:   TransformType.DCT_I,
+            TransformType.IDCT_II:  TransformType.DCT_II,
+            TransformType.IDCT_III: TransformType.DCT_III,
+            TransformType.IDCT_IV:  TransformType.DCT_IV,
+            TransformType.IDST_I:   TransformType.DST_I,
+            TransformType.IDST_II:  TransformType.DST_II,
+            TransformType.IDST_III: TransformType.DST_III,
+            TransformType.IDST_IV:  TransformType.DST_IV,
+        }
+        inverse_transforms = ()
+        for tr in transforms:
+            if (tr not in known_inverse_transforms):
+                msg='Unknown transform {}.'.format(tr)
+                raise NotImplementedError(msg)
+            itr = known_inverse_transforms[tr]
+            inverse_transforms += (itr,)
+        return inverse_transforms
+
+    @classmethod
+    def get_conjugate_inverse_transforms(cls, *transforms):
+        """Get the conjugate inverse TransformType (ie. inverse for odd derivatives)."""
+        known_conjugate_inverse_transforms = {
+            TransformType.NONE:     TransformType.NONE,
+
+            TransformType.FFT:      TransformType.IFFT,
+            TransformType.RFFT:     TransformType.IRFFT,
+            TransformType.DST_I:    TransformType.IDCT_I,
+            TransformType.DST_II:   TransformType.IDCT_III,
+            TransformType.DST_III:  TransformType.IDCT_II,
+            TransformType.DST_IV:   TransformType.IDCT_IV,
+            TransformType.DCT_I:    TransformType.IDST_I,
+            TransformType.DCT_II:   TransformType.IDST_III,
+            TransformType.DCT_III:  TransformType.IDST_II,
+            TransformType.DCT_IV:   TransformType.IDST_IV,
+
+            TransformType.IFFT:     TransformType.FFT,
+            TransformType.IRFFT:    TransformType.RFFT,
+            TransformType.IDST_I:   TransformType.DCT_I,
+            TransformType.IDST_III: TransformType.DCT_II,
+            TransformType.IDST_II:  TransformType.DCT_III,
+            TransformType.IDST_IV:  TransformType.DCT_IV,
+            TransformType.IDCT_I:   TransformType.DST_I,
+            TransformType.IDCT_III: TransformType.DST_II,
+            TransformType.IDCT_II:  TransformType.DST_III,
+            TransformType.IDCT_IV:  TransformType.DST_IV,
+        }
+        conjugate_inverse_transforms = ()
+        for tr in transforms:
+            if (tr not in known_conjugate_inverse_transforms):
+                msg='Unknown transform {}.'.format(tr)
+                raise NotImplementedError(msg)
+            citr = known_conjugate_inverse_transforms[tr]
+            conjugate_inverse_transforms += (citr,)
+        return conjugate_inverse_transforms
+
+
+def make_multivariate_trigonometric_polynomial(Xl, Xr, lboundaries, rboundaries, N):
+    """
+    Build a tensor product of trigonometric polynomials satisfying boundary conditions on each axis.
+    
+    lboundaries: np.ndarray of BoundaryCondition
+    rboundaries: np.ndarray of BoundaryCondition
+    other parameters: scalar or array_like of the same size as boundary arrays
+    
+    All parameters are expanded to the size of the length of prescribed boundaries.
+    See make_trigonometric_polynomial for more informations about parameters.
+    
+    This method returns a tuple (P,Y) where:
+
+    P is a sympy expression representing a multivariate trigonometric polynomials in variables
+    Y = (y0, y1, ..., yd)
+    
+    P(Y) = P0(y0) * P1(y1) * ... * Pd(yd) 
+    
+    *d = lboundaries.size-1 = rboundaries.size-1
+
+    *P0 is a trigonometric polynomial of order N[0] that satisfies (lboundaries[0], rboundaries[0])
+     on domain [Xl[0], Xr[0]].
+    
+    *P1 is a trigonometric polynomial of order N[1] that satisfies (lboundaries[1], rboundaries[1])
+     on domain [Xl[1], Xr[1]].
+    .
+    .
+    .
+    *Pd is a trigonometric polynomial of order N[d] that satisfies (lboundaries[d], rboundaries[d])
+     on domain [Xl[d], Xr[d]].
+    """
+    check_instance(lboundaries, np.ndarray, values=BoundaryCondition, ndim=1, minsize=1)
+    check_instance(rboundaries, np.ndarray, values=BoundaryCondition, size=lboundaries.size)
+    Xl    = to_tuple(Xl)
+    Xr    = to_tuple(Xr)
+    N     = to_tuple(N)
+    dim = max(len(Xl), len(Xr), len(N), lboundaries.size, rboundaries.size)
+    def extend(t):
+        if (len(t)==1):
+            t*=dim
+        return t
+    Xl, Xr, N = extend(Xl), extend(Xr), extend(N)
+    check_instance(Xl,    tuple, size=dim)
+    check_instance(Xr,    tuple, size=dim)
+    check_instance(N,     tuple, values=int,   size=dim)
+    assert lboundaries.size==rboundaries.size==dim
+    assert all(xl<xr for (xl,xr) in zip(Xl,Xr))
+    assert all(n>=1 for n in N)
+    
+    _,Y = tensor_symbol('y', shape=(dim,))
+    P = 1
+    for (xl,xr,lb,rb,n,yi) in zip(Xl, Xr, lboundaries, rboundaries, N, Y):
+        Px, xi = make_trigonometric_polynomial(Xl=xl, Xr=xr, lboundary=lb, rboundary=rb, N=n)
+        Py = Px.xreplace({xi:yi})
+        P *= Py
+    return (P, Y)
+
+def make_multivariate_polynomial(Xl, Xr, lboundaries, rboundaries, N, order):
+    """
+    Build a tensor product of polynomials satisfying boundary conditions on each axis.
+    
+    lboundaries: np.ndarray of BoundaryCondition
+    rboundaries: np.ndarray of BoundaryCondition
+    other parameters: scalar or array_like of the same size as boundary arrays
+    
+    All parameters are expanded to the size of the length of prescribed boundaries.
+    See make_polynomial for more informations about parameters.
+    
+    This method returns a tuple (P,Y) where:
+
+    P is a sympy expression representing a multivariate polynomials in variables
+    Y = (y0, y1, ..., yd)
+    
+    P(Y) = P0(y0) * P1(y1) * ... * Pd(yd) 
+    
+    *d = lboundaries.size-1 = rboundaries.size-1
+
+    *P0 is a polynomial of order N[0] that satisfies (lboundaries[0], rboundaries[0])
+     on domain [Xl[0], Xr[0]] up to order order[0].
+    
+    *P1 is a polynomial of order N[1] that satisfies (lboundaries[1], rboundaries[1])
+     on domain [Xl[1], Xr[1]] up to order order[1].
+    .
+    .
+    .
+    *Pd is a polynomial of order N[d] that satisfies (lboundaries[d], rboundaries[d])
+     on domain [Xl[d], Xr[d]] up to order order[d].
+    """
+    check_instance(lboundaries, np.ndarray, values=BoundaryCondition, ndim=1, minsize=1)
+    check_instance(rboundaries, np.ndarray, values=BoundaryCondition, size=lboundaries.size)
+    Xl    = to_tuple(Xl)
+    Xr    = to_tuple(Xr)
+    N     = to_tuple(N)
+    order = to_tuple(order)
+    dim = max(len(Xl), len(Xr), len(N), len(order), lboundaries.size, rboundaries.size)
+    def extend(t):
+        if (len(t)==1):
+            t*=dim
+        return t
+    Xl, Xr, N, order = extend(Xl), extend(Xr), extend(N), extend(order)
+    check_instance(Xl,    tuple, size=dim)
+    check_instance(Xr,    tuple, size=dim)
+    check_instance(N,     tuple, values=int,   size=dim)
+    check_instance(order, tuple, values=int,   size=dim)
+    assert lboundaries.size==rboundaries.size==dim
+    assert all(xl<xr for (xl,xr) in zip(Xl,Xr))
+    assert all(o>=2 for o in order)
+    assert all(n>2*o for (o,n) in zip(order, N))
+    
+    _,Y = tensor_symbol('y', shape=(dim,))
+    P = 1
+    for (xl,xr,lb,rb,n,o,yi) in zip(Xl, Xr, lboundaries, rboundaries, N, order, Y):
+        Px, xi = make_polynomial(Xl=xl, Xr=xr, lboundary=lb, rboundary=rb, N=n, order=o)
+        Py = Px.xreplace({xi:yi})
+        P *= Py
+    return (P, Y)
+
+
+def make_polynomial(Xl, Xr, lboundary, rboundary, N, order):
+    """
+    Build a polynom of order N-1 between on domain [Xl, Xr] that verifies
+    prescribed left and right boundary conditions up to a certain order.
+    
+    Conditions:
+      Xl < Xr
+      order >= 2
+      N > 2*order > 4
+
+    Valid boundary conditions are:
+        (PERIODIC, PERIODIC)           dPi/dxi(Xl) - dPi/dxi(Xr) = 0
+        (HDIRICHLET, HDIRICHLET)       dPp/dxi(Xl) = dPp/dxi(Xr) = 0 for even derivatives (i%2==0)
+        (HDIRICHLET, HNEUMANN)         mix of the 2nd and 4th conditions
+        (HNEUMANN,   HDIRICHLET)       mix of the 2nd and 4th conditions
+        (HNEUMANN,   HNEUMANN)         dPi/dxi(Xl) = dPi/dxi(Xr) = 0 for odd derivatives (i%2==1)
+
+    Return (P, X) where P is a sympy expression that represent the polynomial and X is the
+    corresponding sympy.Symbol.
+    """
+
+    check_instance(lboundary, BoundaryCondition)
+    check_instance(rboundary, BoundaryCondition)
+    check_instance(N, int)
+    check_instance(order, int)
+    
+    x = sm.Symbol('x')
+    a, A = tensor_symbol('a', shape=(N,))
+    
+    def rand(*n):
+        return 2.0*(np.random.rand(*n)-0.5)
+    
+    K = 2*order
+    assert Xl<Xr
+    assert order>=2
+    assert N>K
+
+    if (N>K):
+        a[K+1:] = rand(N-K-1)
+    if (lboundary, rboundary) == ('DIRICHLET', 'DIRICHLET'):
+        a[K] = rand()
+    else:
+        a[0] = rand()
+
+    P = sum(ai*(x**i) for (i,ai) in enumerate(a))
+
+    Pd = [P]
+    for i in xrange(K):
+        Pd.append(Pd[-1].diff(x))
+
+    eqs = []
+    for i in xrange(order):
+        if (lboundary is BoundaryCondition.PERIODIC):
+            leq = Pd[2*i].xreplace({x:Xl}) - Pd[2*i].xreplace({x:Xr})
+        elif (lboundary is BoundaryCondition.HOMOGENEOUS_NEUMANN):
+            leq = Pd[2*i+1].xreplace({x:Xl})
+        elif (lboundary is BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+            leq = Pd[2*i].xreplace({x:Xl})
+        else:
+            msg='Unknown left boundary condition {}.'.format(lboundary)
+            raise NotImplementedError(msg)
+        
+        if (rboundary is BoundaryCondition.PERIODIC):
+            req = Pd[2*i+1].xreplace({x:Xl}) - Pd[2*i+1].xreplace({x:Xr})
+        elif (rboundary is BoundaryCondition.HOMOGENEOUS_NEUMANN):
+            req = Pd[2*i+1].xreplace({x:Xr})
+        elif (rboundary is BoundaryCondition.HOMOGENEOUS_DIRICHLET):
+            req = Pd[2*i].xreplace({x:Xr})
+        else:
+            msg='Unknown right boundary condition {}.'.format(lboundary)
+            raise NotImplementedError(msg)
+
+        if (leq.free_symbols):
+            eqs.append(leq)
+        if (req.free_symbols):
+            eqs.append(req)
+
+    sol = sm.solve(eqs)
+    P = P.xreplace(sol)
+    sol.update({ai:np.random.rand() for ai in P.free_symbols.intersection(A)})
+    P = P.xreplace(sol)
+
+    P0 = sm.lambdify(x, sm.horner(P))
+    X = np.linspace(Xl, Xr, 1000)
+    m,M = np.min(P0(X)), np.max(P0(X))
+    P /= (M-m)
+    
+    return sm.horner(P), x
+
+
+def make_trigonometric_polynomial(Xl, Xr, lboundary, rboundary, N):
+    """
+    Build a real trigonometric polynomial of order N-1 
+    between on domain [Xl, Xr] that verifies prescribed left and right 
+    boundary conditions.
+    
+    Conditions:
+      Xl < Xr
+      N >= 1
+
+    Valid boundary conditions are:
+        (PERIODIC, PERIODIC)           
+        (HDIRICHLET, HDIRICHLET)       
+        (HDIRICHLET, HNEUMANN)         
+        (HNEUMANN,   HDIRICHLET)       
+        (HNEUMANN,   HNEUMANN)         
+
+    Return (P, X) where P is a sympy expression that represent the polynomial and X is the
+    corresponding sympy.Symbol.
+    """
+    assert N>=1
+    assert Xl<Xr
+
+    def r(*n):
+        return 2.0*(np.random.rand(*n)-0.5)
+
+    x = sm.Symbol('x')
+    y = (x-Xl)/(Xr-Xl)*(2*sm.pi)
+
+    boundaries = (lboundary, rboundary)
+    if (boundaries == (BoundaryCondition.PERIODIC, 
+                       BoundaryCondition.PERIODIC)):
+        fn = lambda n: r()*sm.cos(n*y+sm.pi*r()) + r()*sm.sin(n*y+sm.pi*r())
+    elif (boundaries == (BoundaryCondition.HOMOGENEOUS_DIRICHLET, 
+                         BoundaryCondition.HOMOGENEOUS_DIRICHLET)):
+        fn = lambda n: r()*sm.sin(n*y)
+    elif (boundaries == (BoundaryCondition.HOMOGENEOUS_DIRICHLET, 
+                         BoundaryCondition.HOMOGENEOUS_NEUMANN)):
+        fn = lambda n: r()*sm.sin((4*n-1)/4.0*y)
+    elif (boundaries == (BoundaryCondition.HOMOGENEOUS_NEUMANN,
+                         BoundaryCondition.HOMOGENEOUS_DIRICHLET)):
+        fn = lambda n: r()*sm.cos((4*n-1)/4.0*y)
+    elif (boundaries == (BoundaryCondition.HOMOGENEOUS_NEUMANN,
+                         BoundaryCondition.HOMOGENEOUS_NEUMANN)):
+        fn = lambda n: r()*sm.cos(n*y)
+    else:
+        msg='Unknown right boundary condition pair {}.'.format(boundaries)
+        raise NotImplementedError(msg)
+    
+    P = sum(fn(i) for i in xrange(1, N+1))
+    P0 = sm.lambdify(x, P)
+    X = np.linspace(Xl, Xr, 1000)
+    m, M = np.min(P0(X)), np.max(P0(X))
+    P *= 2.0/(M-m)
+    
+    return (P, x)
+
+
+
+if __name__ == '__main__':
+    from hysop.tools.sympy_utils import round_expr
+    P = make_trigonometric_polynomial(-1.0, +1.0, BoundaryCondition.HOMOGENEOUS_DIRICHLET,
+                                                  BoundaryCondition.HOMOGENEOUS_NEUMANN, 10)[0]
+    print round_expr(P,2)
+    print
+    P = make_polynomial(-1.0,+1.0, BoundaryCondition.HOMOGENEOUS_NEUMANN, 
+                                   BoundaryCondition.HOMOGENEOUS_DIRICHLET, 10, 2)[0]
+    print round_expr(P,2)
+    print
+    lboundaries = np.asarray([BoundaryCondition.HOMOGENEOUS_NEUMANN, BoundaryCondition.PERIODIC])
+    rboundaries = np.asarray([BoundaryCondition.HOMOGENEOUS_DIRICHLET, BoundaryCondition.PERIODIC])
+    P = make_multivariate_trigonometric_polynomial(-1.0,+1.0, lboundaries, rboundaries, (3,5))[0]
+    print round_expr(P,2)
+    print
+    P = make_multivariate_polynomial(-1.0,+1.0, lboundaries, rboundaries, (6,10), 2)[0]
+    print round_expr(P,2)
+    print
+
diff --git a/hysop/tools/sympy_utils.py b/hysop/tools/sympy_utils.py
index 8dd7216d55b76fe1c3821e909ce4d20d19772445..0a0b0e9232e47208bebd756d245ea0b4d38134b1 100644
--- a/hysop/tools/sympy_utils.py
+++ b/hysop/tools/sympy_utils.py
@@ -1,6 +1,6 @@
 
 from hysop.deps import np, sm, copy
-from hysop.tools.types import first_not_None, check_instance
+from hysop.tools.types import first_not_None, check_instance, to_tuple
 
 from sympy.utilities import group
 from sympy.printing.str import StrPrinter, StrReprPrinter
@@ -17,6 +17,21 @@ parenthesis = (u'\u208d', u'\u208e')
 partial = u'\u2202'
 nabla = u'\u2207'
 xsymbol = u'x'
+freq_symbol = greak[12] # nu
+
+def round_expr(expr, num_digits=3):
+    return expr.xreplace({n : round(n, num_digits) for n in 
+        expr.atoms(sm.Float).union(expr.atoms(sm.Rational)).difference(expr.atoms(sm.Integer))})
+def truncate_expr(expr, maxlen=80):
+    assert maxlen>=3
+    parts = sstr(expr).split(' ')
+    assert parts
+    ss=parts.pop(0)
+    while parts and (len(ss+parts[0])<maxlen):
+        ss+=parts.pop(0)
+    if parts:
+        ss+='...'
+    return ss
 
 class CustomStrPrinter(StrPrinter):
     def _print_Derivative(self, expr):
@@ -59,6 +74,8 @@ def enable_pretty_printing():
 class SymbolicBase(object):
     def __new__(cls, name, var_name=None, latex_name=None, 
             pretty_name=None, **kwds):
+        if isinstance(name, unicode):
+            name = name.encode('utf-8')
         if isinstance(pretty_name, unicode):
             pretty_name = pretty_name.encode('utf-8')
         if isinstance(latex_name, unicode):
@@ -79,7 +96,7 @@ class SymbolicBase(object):
         return self._var_name
 
     def _sympystr(self, printer):
-        return self._name
+        return self._pretty_name
     def _latex(self, printer):
         return self._latex_name
     def _ccode(self, printer):
@@ -129,7 +146,13 @@ class AppliedUndef(sm.function.AppliedUndef):
     def _pretty(self, printer):
         return self._pretty_name
     def _sympystr(self, printer):
-        return self._name
+        return self._pretty_name
+    #def _pretty(self, printer):
+        #return '{}({})'.format(self._pretty_name, 
+                                #','.join(printer._print(a) for a in self.args))
+    #def _sympystr(self, printer):
+        #return '{}({})'.format(self._pretty_name, 
+                                #','.join(printer._print(a) for a in self.args))
 
 def subscript(i, with_sign=False):
     """
@@ -180,6 +203,7 @@ def subscripts(ids,sep,with_sign=False,with_parenthesis=False,prefix=''):
     Generate a unicode tuple subscript separated by sep,
     with or without parenthesis, prefix, and signs.
     """
+    ids = to_tuple(ids)
     if with_parenthesis:
         return u'{}{}{}{}'.format(prefix,parenthesis[0],sep.join([subscript(i,with_sign) for i in ids]),parenthesis[1])
     else:
@@ -190,6 +214,7 @@ def exponents(ids,sep,with_sign=False,with_parenthesis=False,prefix=''):
     Generate a unicode tuple exponent separated by sep,
     with or without parenthesis, prefix, and signs.
     """
+    ids = to_tuple(ids)
     if with_parenthesis:
         return u'{}{}{}{}'.format(prefix,parenthesis[0],sep.join([exponent(i,with_sign) for i in ids]),parenthesis[1])
     else:
@@ -235,7 +260,6 @@ def tensor_xreplace(tensor,vars):
                 T[idx] = vars[symbol.name]
     return T
 
-
 def non_eval_xreplace(expr, rule):
     """
     Duplicate of sympy's xreplace but with non-evaluate statement included.
@@ -351,3 +375,8 @@ def get_derivative_variables(expr):
         _vars = tuple(expr.args[1:])
     return _vars
 
+class SetupExprI(object):
+    """Interface for setupable expressions."""
+    def setup(self, work):
+        raise NotImplementedError
+
diff --git a/hysop/tools/types.py b/hysop/tools/types.py
index cdc059a37d243fe92b46ef19363ebf952931de1f..3f127f44000393a126e1d5097757d9b05c947d11 100644
--- a/hysop/tools/types.py
+++ b/hysop/tools/types.py
@@ -14,7 +14,8 @@ class InstanceOf(object):
         return 'InstanceOf({})'.format(self.cls.__name__)
 
 
-def check_instance(val, cls, allow_none=False, **kargs):
+def check_instance(val, cls, allow_none=False,  
+                        check_kwds=True, **kargs):
     """
     Raise a TypeError if val is not an instance of cls.
     cls can be a tuple of types like isinstance(...) capabilities.
@@ -123,7 +124,7 @@ def check_instance(val, cls, allow_none=False, **kargs):
                     raise ValueError(msg)
                 if (maxval is not None) and (maxval and v>maxval):
                     msg='Value contained in given {} has value {} which is greater '
-                    msg+='than the specified minimum value {}.'
+                    msg+='than the specified maximum value {}.'
                     msg=msg.format(cls.__name__, v, maxval)
                     raise ValueError(msg)
     elif isinstance(val, dict):
@@ -236,15 +237,11 @@ def check_instance(val, cls, allow_none=False, **kargs):
             msg=msg.format(val, minval)
             raise ValueError(msg)
         if maxval and val>maxval:
-            msg='Value {} is greater than the specified minimum value {}.'
+            msg='Value {} is greater than the specified maximum value {}.'
             msg=msg.format(val, maxval)
             raise ValueError(msg)
-    if kargs:
-        # Throw away unused arguments but ok
-        if not isinstance(val, dict):
-            ## Throw dict-specific argument unused because val is not a dict
-            kargs.pop('keys')
-    if kargs:
+    
+    if check_kwds and kargs:
         raise RuntimeError('Some arguments were not used ({}).'.format(kargs))
 
 
diff --git a/hysop/topology/cartesian_descriptor.py b/hysop/topology/cartesian_descriptor.py
index 5c3f95c66bbb24016a9f6dd8f3691e7be231354a..80a3315d35be912652d220b3288b5ed6a98be762 100644
--- a/hysop/topology/cartesian_descriptor.py
+++ b/hysop/topology/cartesian_descriptor.py
@@ -2,7 +2,7 @@
 from hysop.tools.types import check_instance
 from hysop.topology.topology_descriptor import TopologyDescriptor
 from hysop.topology.cartesian_topology import CartesianTopology
-from hysop.tools.parameters import Discretization
+from hysop.tools.parameters import CartesianDiscretization
 from hysop.constants import Backend, BoundaryCondition
 from hysop.fields.continuous_field import Field
 from hysop.tools.numpywrappers import npw
@@ -14,9 +14,9 @@ class CartesianTopologyDescriptor(TopologyDescriptor):
     """
 
     __slots__ = ('_mpi_params', '_domain', '_backend', '_extra_kwds',
-                    '_global_resolution', '_space_step')
+                    '_cartesian_discretization', '_space_step')
 
-    def __init__(self, mpi_params, domain, backend, global_resolution, **kwds):
+    def __init__(self, mpi_params, domain, backend, cartesian_discretization, **kwds):
         """
         Initialize a CartesianTopologyDescriptor.
         
@@ -28,36 +28,80 @@ class CartesianTopologyDescriptor(TopologyDescriptor):
         super(CartesianTopologyDescriptor, self).__init__(mpi_params=mpi_params, 
                 domain=domain, backend=backend, **kwds)
         
-        # global_resolution <=> compute global_resolution + domain.periodicity
-        global_resolution = npw.asdimarray(global_resolution).copy()
-        check_instance(global_resolution, np.ndarray, 
-                            size=domain.dim, minval=2)
-
-        space_step = npw.asrealarray(domain.length / (global_resolution - 1))
+        check_instance(cartesian_discretization, CartesianDiscretization)
+        
+        # check cartesian_discretization 
+        if (cartesian_discretization.ghosts > 0).any():
+            msg='No ghost allowed for a topology descriptor.'
+            raise ValueError(msg)
+        
+        global_resolution  = cartesian_discretization.global_resolution
+        grid_resolution    = cartesian_discretization.grid_resolution
+        lboundaries        = cartesian_discretization.lboundaries
+        rboundaries        = cartesian_discretization.rboundaries
+        
+        check_instance(grid_resolution, np.ndarray, size=domain.dim, minval=2)
+        check_instance(global_resolution, np.ndarray, size=domain.dim, minval=2)
+        check_instance(lboundaries, npw.ndarray, dtype=object, 
+                size=domain.dim, values=BoundaryCondition,
+                allow_none=True)
+        check_instance(rboundaries, npw.ndarray, dtype=object, 
+                size=domain.dim, values=BoundaryCondition, 
+                allow_none=True)
+        
+        is_lperiodic = (lboundaries==BoundaryCondition.PERIODIC) 
+        is_rperiodic = (rboundaries==BoundaryCondition.PERIODIC) 
 
-        assert global_resolution.size == domain.dim
+        assert all((grid_resolution + is_lperiodic) == global_resolution)
 
-        npw.set_readonly(global_resolution, space_step)
+        msg='Invalid boundary conditions {} vs {}.'
+        msg=msg.format(lboundaries, rboundaries)
+        assert not (is_lperiodic ^ is_rperiodic).any(), msg
 
-        self._global_resolution = global_resolution
-        self._space_step = space_step
+        # compute space step
+        space_step = npw.asrealarray(domain.length / (global_resolution - 1))
+        npw.set_readonly(space_step)
 
-    def _get_space_step(self):
+        self._cartesian_discretization = cartesian_discretization
+        self._space_step  = space_step
+    
+    @property
+    def global_resolution(self):
+        """Get the global global_resolution of the discretization (logical grid_size)."""
+        return self._cartesian_discretization.global_resolution
+    
+    @property
+    def grid_resolution(self):
+        """Get the global grid resolution of the discretization (effective grid size)."""
+        return self._cartesian_discretization.grid_resolution
+
+    @property
+    def lboundaries(self):
+        """Get the left boundaries."""
+        return self._cartesian_discretization.lboundaries
+    
+    @property
+    def rboundaries(self):
+        """Get the left boundaries."""
+        return self._cartesian_discretization.rboundaries
+    
+    @property
+    def boundaries(self):
+        """Get left and right boundaries."""
+        return (self._cartesian_discretization.lboundaries,
+                self._cartesian_discretization.rboundaries)
+    
+    @property
+    def space_step(self):
         """Get the space step."""
         return self._space_step
-    def _get_resolution(self):
-        """Get the global global_resolution of the discretization."""
-        return self._global_resolution
-
-    space_step = property(_get_space_step)
-    global_resolution = property(_get_resolution)
 
     def match(self, other, invert=False):
         """Test if this descriptor is equivalent to the other one."""
         eq = super(CartesianTopologyDescriptor,self).match(other, invert=False)
         if (eq is NotImplemented) or (not isinstance(other, CartesianTopologyDescriptor)):
             return NotImplemented
-        eq &= (self._global_resolution == other._global_resolution).all()
+        eq &= (self._cartesian_discretization == other._cartesian_discretization)
         if invert:
             return not eq
         else:
@@ -70,48 +114,61 @@ class CartesianTopologyDescriptor(TopologyDescriptor):
 
     def __hash__(self):
         # hash(super(...)) does not work as expected so be call __hash__ directly
-        return super(CartesianTopologyDescriptor,self).__hash__() ^ hash(self._global_resolution.data)
+        h = super(CartesianTopologyDescriptor,self).__hash__() 
+        h ^= hash(self._cartesian_discretization)
+        return h
 
     def __str__(self):
-        return ':CartesianTopologyDescriptor: backend={}, global_resolution={}, domain={}'.format(
-                    self.backend, self.global_resolution, 
-                    self.domain.tag)
-
-    @staticmethod
-    def build_descriptor(backend, operator, field, handle, **kwds):
+        return ':CartesianTopologyDescriptor: backend={}, domain={}, grid_resolution={}, bc=[{}]'.format(
+                    self.backend, self.domain.full_tag,
+                    self.grid_resolution, 
+                     ','.join(('{}/{}'.format(
+                         str(lb).replace('HOMOGENEOUS_','')[:3],
+                         str(rb).replace('HOMOGENEOUS_','')[:3]) 
+                         for (lb,rb) in zip(*self.boundaries))))
+
+    @classmethod
+    def build_descriptor(cls, backend, operator, field, handle, **kwds):
         from hysop.core.graph.computational_operator import ComputationalGraphOperator
         check_instance(backend, Backend)
         check_instance(operator, ComputationalGraphOperator)
         check_instance(field, Field)
         check_instance(handle, CartesianTopologyDescriptors)
-        if isinstance(handle, (tuple,list,np.ndarray,Discretization)):
+        if isinstance(handle, (tuple,list,np.ndarray,CartesianDiscretization)):
             if not hasattr(operator, 'mpi_params'):
                 msg='mpi_params has not been set in operator {}.'.format(operator.name)
                 raise RuntimeError(msg)
-            if isinstance(handle, Discretization):
-                msg='A CartesianTopology topology descriptor should not contain any ghosts, '
-                msg+='they will be determined during the get_field_requirements() in the '
-                msg+=' operator initialization step to minimize the number of topologies created.'
-                msg+='\nIf you want to impose a specific topology, you can directly pass a '
-                msg+='CartesianTopology instance into operator\'s input or output variables '
-                msg+='dictionnary instead.'
+            if isinstance(handle, CartesianDiscretization):
                 if (handle.ghosts.sum() > 0):
+                    msg='A CartesianTopology topology descriptor should not contain any ghosts, '
+                    msg+='they will be determined during the get_field_requirements() in the '
+                    msg+=' operator initialization step to minimize the number of topologies created.'
+                    msg+='\nIf you want to impose a specific topology, you can directly pass a '
+                    msg+='CartesianTopology instance into operator\'s input or output variables '
+                    msg+='dictionnary instead.'
+                    raise ValueError(msg)
+                if (handle.lboundaries is not None) or (handle.rboundaries is not None):
+                    msg='A CartesianTopology topology descriptor should not contain any boundary conditions, '
+                    msg+='they will be automatically determined from continuous fields.'
                     raise ValueError(msg)
                 global_resolution = handle.resolution
             else: 
                 global_resolution = handle
 
-            global_resolution = npw.asdimarray(global_resolution)
-            return CartesianTopologyDescriptor(
-                    backend=backend, 
+            cartesian_discretization = CartesianDiscretization(resolution=global_resolution,
+                    lboundaries=field.lboundaries, rboundaries=field.rboundaries,
+                    ghosts=None)
+
+            return CartesianTopologyDescriptor(backend=backend, 
                     domain=field.domain, 
                     mpi_params=operator.mpi_params,
-                    global_resolution=global_resolution,
+                    cartesian_discretization = cartesian_discretization,
                     **kwds)
         elif isinstance(handle, CartesianTopologyDescriptor):
             return handle
         else:
-            # handle is a CartesianTopology instance, ghosts can be imposed freely by user here
+            # handle is a CartesianTopology instance, ghosts and boundary conditions
+            # can be imposed freely by user here.
             return handle
     
     def choose_topology(self, known_topologies, **kwds):
@@ -120,7 +177,9 @@ class CartesianTopologyDescriptor(TopologyDescriptor):
         If None is returned, create_topology will be called instead.
         """
         if known_topologies:
-            return known_topologies[0]
+            ordered_topologies = sorted(known_topologies, 
+                                            key=lambda topo: sum(topo.ghosts))
+            return ordered_topologies[0]
         else:
             return None
 
@@ -131,7 +190,10 @@ class CartesianTopologyDescriptor(TopologyDescriptor):
         by operators on variables and solved during operator's
         method get_field_requirements().
         """
-        discretization = Discretization(self.global_resolution, ghosts)
+        discretization = CartesianDiscretization(resolution=self.grid_resolution, 
+                                                 lboundaries=self.lboundaries, 
+                                                 rboundaries=self.rboundaries,
+                                                 ghosts=ghosts)
         return CartesianTopology(domain=self.domain, 
                                  discretization=discretization, 
                                  mpi_params=self.mpi_params,
@@ -140,7 +202,8 @@ class CartesianTopologyDescriptor(TopologyDescriptor):
                                  **self.extra_kwds)
 
 
-CartesianTopologyDescriptors = (CartesianTopology, CartesianTopologyDescriptor, Discretization, tuple, list, np.ndarray, type(None))
+CartesianTopologyDescriptors = (CartesianTopology, CartesianTopologyDescriptor, CartesianDiscretization, 
+                                tuple, list, np.ndarray, type(None))
 """
 Instance of those types can be used to create a CartesianTopologyDescriptor.
 Thus they can be passed in the variables of each operator supporting
diff --git a/hysop/topology/cartesian_topology.py b/hysop/topology/cartesian_topology.py
index 1bd906236c90b57e1fc5dac51a6b508aeef9e64b..6a64a61bc6551a605b722b7b9945f48e3fd64615 100644
--- a/hysop/topology/cartesian_topology.py
+++ b/hysop/topology/cartesian_topology.py
@@ -8,7 +8,7 @@ from hysop.tools.transposition_states import TranspositionState
 from hysop.domain.box import Box, BoxView
 from hysop.core.mpi import MPI
 from hysop.tools.types import check_instance, to_tuple, first_not_None
-from hysop.tools.parameters import Discretization, MPIParams
+from hysop.tools.parameters import CartesianDiscretization, MPIParams
 from hysop.tools.misc import Utils, prod
 from hysop.tools.decorators import debug, deprecated
 from hysop.tools.numpywrappers import npw
@@ -237,8 +237,11 @@ class CartesianTopologyView(TopologyView):
     
     # ATTRIBUTE GETTERS
     def _get_global_resolution(self):
-        """Returns global resolution of the discretization."""
-        return self._proc_transposed(self._topology._discretization.resolution)
+        """Returns global resolution of the discretization (logical grid size)."""
+        return self._proc_transposed(self._topology._discretization.global_resolution)
+    def _get_grid_resolution(self):
+        """Returns grid resolution of the discretization (effective grid size)."""
+        return self._proc_transposed(self._topology._discretization.grid_resolution)
     def _get_ghosts(self):
         """Returns ghosts of the discretization."""
         return self._proc_transposed(self._topology._discretization.ghosts)
@@ -335,6 +338,7 @@ class CartesianTopologyView(TopologyView):
         return np.where(self._get_is_periodic() == True)[0].astype(np.int32)
     
     global_resolution = property(_get_global_resolution)
+    grid_resolution = property(_get_grid_resolution)
     ghosts = property(_get_ghosts)
 
     comm = property(_get_comm)
@@ -371,15 +375,19 @@ class CartesianTopologyView(TopologyView):
         Returns a short description of the current TopologyView.
         Short version of long_description().
         """
-        s='{}[domain={}, pcoords={}, pshape={}, '
-        s+='shape={}, ghosts={}, backend={}]'
+        s='{}[domain={}, backend={}, pcoords={}, pshape={}, '
+        s+='grid_resolution={}, ghosts={}, bc=({})]'
         s = s.format(
                 self.full_tag, 
                 self.domain.domain.full_tag, 
+                self.backend.kind,
                 self.proc_coords, self.proc_shape, 
-                '[{}]'.format(','.join(str(s) for s in self.global_resolution)),
+                '[{}]'.format(','.join(str(s) for s in self.grid_resolution)),
                 '[{}]'.format(','.join(str(g) for g in self.ghosts)),
-                self.backend.kind)
+                 ','.join(('{}/{}'.format(
+                     str(lb).replace('HOMOGENEOUS_','')[:3],
+                     str(rb).replace('HOMOGENEOUS_','')[:3]) 
+                     for (lb,rb) in zip(*self.mesh.global_boundaries))))
         return s
 
     def long_description(self):
@@ -402,7 +410,7 @@ class CartesianTopologyView(TopologyView):
         s += prepend(str(self.proc_neighbour_ranks), ' '*4) + '\n'
         s += prepend('*'+str(self.domain), ' '*2)
         s += prepend('*'+str(self.mesh),' '*2)
-        s += '=================================\n'
+        s += '===================================\n'
         return s
 
     def can_communicate_with(self, target):
@@ -412,7 +420,7 @@ class CartesianTopologyView(TopologyView):
         - all processes in current are in target
         - both topologies belong to the same mpi task
         """
-        if self.topology == target.topology:
+        if (self.topology == target.topology):
             return True
 
         msg = 'You try to connect topologies belonging to'
@@ -456,8 +464,9 @@ class CartesianTopology(CartesianTopologyView, Topology):
         ----------
         domain : :class:`~hysop.domain.box.Box`
             The box geometry on which the cartesian topology is defined.
-        discretization: :class:`~hysop.tools.parameters.Discretization`
-            Description of the global space discretization of the box (resolution and ghosts).
+        discretization: :class:`~hysop.tools.parameters.CartesianDiscretization`
+            Description of the global space discretization of the box (resolution, ghosts,
+            and boundary conditions).
         mpi_params : :class:`~hysop.tools.parameters.MPIParams`, optional
             MPI parameters (comm, task ...).
             If not specified, comm = domain.task_comm, task = domain.curent_task()
@@ -469,7 +478,7 @@ class CartesianTopology(CartesianTopologyView, Topology):
         cart_shape: list or array of int, optional
             MPI grid layout, should be sized as the domain dimension.
         is_periodic : tuple, list or array of bool, optional
-            MPI grid periodicity, overrides domain boundary conditions.
+            MPI grid periodicity, *overrides* discretization boundary conditions.
         cutdirs: list or array of bool, optional
             Set which directions may be distributed,
             cutdirs[dir] = True allow MPI to distribute data along dir.
@@ -488,7 +497,7 @@ class CartesianTopology(CartesianTopologyView, Topology):
         global_resolution: np.ndarray of HYSOP_INTEGER
             Resolution of the global mesh (as given in the discretization parameter).
         ghosts: np.ndarray of HYSOP_INTEGER
-            Discretization ghosts of local-to-process mesh (as given in 
+            CartesianDiscretization ghosts of local-to-process mesh (as given in 
             the discretization parameter).
         mesh: :class:`~hysop.domain.mesh.CartesianMeshView`:
             Local mesh on the current mpi process.
@@ -576,13 +585,13 @@ class CartesianTopology(CartesianTopologyView, Topology):
 
         # prepare MPI processes layout
         (cart_dim, cart_size, proc_shape, is_periodic, is_distributed) = \
-            cls._check_topo_parameters(mpi_params, domain,
+            cls._check_topo_parameters(mpi_params, domain, discretization,
                 cart_shape, cutdirs, cart_dim, is_periodic, cartesian_topology)
 
         # double check types, to be sure RegisteredObject will work as expected
         check_instance(mpi_params, MPIParams)
         check_instance(domain, Box)
-        check_instance(discretization, Discretization)
+        check_instance(discretization, CartesianDiscretization)
         check_instance(cart_dim,  int)
         check_instance(cart_size, int)
         check_instance(proc_shape,     np.ndarray, dtype=HYSOP_INTEGER)
@@ -613,36 +622,6 @@ class CartesianTopology(CartesianTopologyView, Topology):
 
         return obj
 
-    def topology_like(self, backend=None, global_resolution=None, ghosts=None, 
-            mpi_params=None, **kwds):
-        """Return a topology like this object, possibly altered."""
-        global_resolution = first_not_None(global_resolution, self._discretization.resolution)
-        ghosts            = first_not_None(ghosts,            self._discretization.ghosts)
-        backend           = first_not_None(backend,           self._backend)
-        discretization    = Discretization(resolution=global_resolution, ghosts=ghosts)
-
-        # find out the target mpi_params
-        from hysop.core.arrays.all import OpenClArrayBackend
-        if isinstance(backend, OpenClArrayBackend): 
-            if (mpi_params is not None) and (mpi_params != backend.cl_env.mpi_params):
-                msg='Backend mpi params mismatch.'
-                raise RuntimeError(msg)
-            mpi_params = backend.cl_env.mpi_params
-        mpi_params = first_not_None(mpi_params, self.mpi_params)
-
-        if (mpi_params == self.mpi_params):
-            # we can use the same cartesian communicator
-            return CartesianTopology(domain=self._domain, mpi_params=mpi_params,
-                    discretization=discretization, backend=backend, 
-                    cart_dim=self.cart_dim, cart_shape=self.cart_shape,
-                    is_periodic=self.is_periodic, cartesian_topology=self.cart_comm, **kwds)
-        else:
-            # we need to create a new cartesian communicator with the same shape
-            return CartesianTopology(domain=self._domain, mpi_params=mpi_params,
-                    discretization=discretization, backend=backend, 
-                    cart_dim=self.cart_dim, cart_shape=self.cart_shape,
-                    is_periodic=self.is_periodic, cartesian_topology=None, **kwds)
-       
     def __initialize(self, domain, discretization, 
             cart_dim, cart_size, proc_shape, 
             is_periodic, is_distributed, 
@@ -679,7 +658,6 @@ class CartesianTopology(CartesianTopologyView, Topology):
         if __debug__:
             self.__check_vars()
 
-
     def __check_vars(self):
         # check variables and properties at the same time
         check_instance(self.global_resolution, np.ndarray, HYSOP_INTEGER)
@@ -711,9 +689,39 @@ class CartesianTopology(CartesianTopologyView, Topology):
         check_instance(self.domain, BoxView)
         check_instance(self.mesh, CartesianMeshView)
 
+    def topology_like(self, backend=None, grid_resolution=None, ghosts=None, 
+            lboundaries=None, rboundaries=None, mpi_params=None, 
+            cart_shape=None, **kwds):
+        """Return a topology like this object, possibly altered."""
+        assert ('global_resolution' not in kwds), 'Specify grid_resolution instead.'
+        grid_resolution   = first_not_None(grid_resolution,   self._discretization.grid_resolution)
+        ghosts            = first_not_None(ghosts,            self._discretization.ghosts)
+        lboundaries       = first_not_None(lboundaries,       self._discretization.lboundaries)
+        rboundaries       = first_not_None(rboundaries,       self._discretization.rboundaries)
+        backend           = first_not_None(backend,           self._backend)
+        discretization    = CartesianDiscretization(resolution=grid_resolution, ghosts=ghosts,
+                                                    lboundaries=lboundaries, rboundaries=rboundaries)
+
+        # find out the target mpi_params
+        from hysop.core.arrays.all import OpenClArrayBackend
+        if isinstance(backend, OpenClArrayBackend): 
+            if (mpi_params is not None) and (mpi_params != backend.cl_env.mpi_params):
+                msg='Backend mpi params mismatch.'
+                raise RuntimeError(msg)
+            mpi_params = backend.cl_env.mpi_params
+        mpi_params = first_not_None(mpi_params, self.mpi_params)
+
+        if (self.domain.dim == grid_resolution.size):
+            cart_shape = first_not_None(cart_shape, self.proc_shape)
+
+        return CartesianTopology(domain=self._domain, mpi_params=mpi_params,
+                discretization=discretization, backend=backend, 
+                cart_shape=self.proc_shape, 
+                cartesian_topology=None, **kwds)
+       
     
     @classmethod
-    def _check_topo_parameters(cls, mpi_params, domain,
+    def _check_topo_parameters(cls, mpi_params, domain, discretization,
                                shape, cutdirs,
                                dim, is_periodic,
                                cartesian_topology):
@@ -792,7 +800,14 @@ class CartesianTopology(CartesianTopologyView, Topology):
             cls._optimize_shape(shape)
         
         if (is_periodic is None):
-            is_periodic = tuple([True, ] * domain_dim)
+            try:
+                is_periodic = discretization.periodicity
+            except AttributeError:
+                msg ='Given CartesianDiscretization was not setup correctly:'
+                msg+='\n  => Boundary conditions have not been set.'
+                msg+='\n{}'
+                msg=msg.format(discretization)
+                raise ValueError(msg)
         
         shape       = npw.asintegerarray(shape)
         is_periodic = (np.asarray(is_periodic) != 0)
@@ -806,8 +821,7 @@ class CartesianTopology(CartesianTopologyView, Topology):
         cart_dim  = cart_shape.size
         cart_size = prod(cart_shape)
         
-        is_periodic    = is_periodic * is_distributed
-        is_distributed = is_distributed
+        is_periodic = is_periodic * is_distributed
 
         assert (cart_dim>0) and (cart_dim <= domain_dim)
         assert prod(cart_shape) == prod(shape)
@@ -909,8 +923,8 @@ class CartesianTopology(CartesianTopologyView, Topology):
 
     def _compute_mesh(self, domain, discretization):
         assert isinstance(domain, Box)
-        assert isinstance(discretization, Discretization)
-        assert self.domain_dim == discretization.resolution.size, \
+        assert isinstance(discretization, CartesianDiscretization)
+        assert (self.domain_dim == discretization.grid_resolution.size), \
             'The resolution size differs from the domain dimension.'
 
         proc_coords = self._proc_coords
@@ -918,14 +932,17 @@ class CartesianTopology(CartesianTopologyView, Topology):
         
         # Find out dimension and periodic axes of the domain 
         domain_dim  = domain.dim
-        is_periodic = domain.periodicity
+        periodicity = discretization.periodicity
         
-        # Remove 1 point on each periodic axe because of periodicity
-        computational_grid_resolution = discretization.resolution - is_periodic
+        # /!\ Now we assume that the user gives us the grid resolutionn
+        #     and not the global_resolution as it used to be.
+        #     We do not remove 1 point on each periodic axe because of periodicity
+        computational_grid_resolution = discretization.grid_resolution
 
-        # Number of "computed" points (i.e. excluding ghosts/boundaries).
+        # Number of "computed" points (i.e. excluding ghosts).
         pts_noghost    = npw.dim_zeros((domain_dim))
         pts_noghost[:] = computational_grid_resolution // proc_shape
+        assert all(computational_grid_resolution >= proc_shape)
 
         # If any, remaining points are added on the mesh of the last process.
         remaining_points = npw.dim_zeros(domain_dim)
@@ -967,6 +984,22 @@ class CartesianTopology(CartesianTopologyView, Topology):
         from hysop.fields.cartesian_discrete_field import CartesianDiscreteScalarField, \
                                                           TmpCartesianDiscreteScalarField
         check_instance(field, ScalarField)
+
+        if (field.lboundaries != self._discretization.lboundaries).any() or \
+           (field.rboundaries != self._discretization.rboundaries).any():
+            msg=\
+'''
+Cannot discretize a field with cartesian boundary conditions:'
+  lboundaries: {}
+  rboundaries: {}
+On a cartesian topology with different boundary conditions:
+  lboundaries: {}
+  rboundaries: {}
+'''.format(field.lboundaries, field.rboundaries,
+           self._discretization.lboundaries,
+           self._discretization.rboundaries)
+            raise RuntimeError(msg)
+
         if field.is_tmp:
             return TmpCartesianDiscreteScalarField(field=field, topology=self)
         else:
diff --git a/hysop/topology/topology.py b/hysop/topology/topology.py
index 9a37cee922767cf4a25830f4cef876bd6cffcad1..77300f5eec1321f0b0173d7e0397340f8b3ad44a 100644
--- a/hysop/topology/topology.py
+++ b/hysop/topology/topology.py
@@ -13,8 +13,8 @@ from hysop.constants import HYSOP_MPI_REAL, HYSOP_MPI_ORDER
 from hysop.domain.domain import Domain
 from hysop.core.mpi import MPI
 from hysop.core.arrays.array_backend import ArrayBackend
-from hysop.tools.types import check_instance, to_tuple
-from hysop.tools.parameters import Discretization, MPIParams
+from hysop.tools.types import check_instance, to_tuple, first_not_None
+from hysop.tools.parameters import MPIParams
 from hysop.tools.misc import Utils
 from hysop.tools.decorators import debug 
 from hysop.tools.numpywrappers import npw
@@ -268,18 +268,7 @@ class Topology(RegisteredObject):
 
     At the time, only CartesianTopology topologies with cartesian meshes are available.
 
-    Example :
-    \code
-    >>> from hysop.topology.cartesian_topology import CartesianTopology
-    >>> from hysop.tools.parameters import Discretization
-    >>> from hysop.domain.box import Box
-    >>> dom = Box()
-    >>> r = Discretization([33, 33, 33])
-    >>> topo = CartesianTopology(dom, dim=2, discretization=r)
-    >>>
-    \endcode
     For details about topologies see HySoP User Manual.
-
     You can also find examples of topologies instanciation in test_topology.py.
     """
 
@@ -373,7 +362,8 @@ class Topology(RegisteredObject):
             elif (backend == Backend.OPENCL):
                 from hysop.backend.device.opencl.opencl_tools import get_or_create_opencl_env
                 from hysop.core.arrays.all import OpenClArrayBackend
-                cl_env = cl_env or get_or_create_opencl_env(mpi_params)
+                if (cl_env is None):
+                    cl_env = get_or_create_opencl_env(mpi_params)
                 assert cl_env.mpi_params == mpi_params
                 backend = OpenClArrayBackend.get_or_create(cl_env=cl_env, queue=queue, allocator=allocator)
                 assert backend.cl_env.mpi_params == mpi_params
diff --git a/hysop/topology/topology_descriptor.py b/hysop/topology/topology_descriptor.py
index 6c0af020770f33704fd09b8b0da0692aee2363b3..ff0bd717eeab81cfd3deda47c2094a2959318236 100644
--- a/hysop/topology/topology_descriptor.py
+++ b/hysop/topology/topology_descriptor.py
@@ -31,6 +31,9 @@ class TopologyDescriptor(object):
         self._backend=backend
         self._extra_kwds = frozenset(kwds.items())
 
+        if ('cl_env' in kwds):
+            assert kwds['cl_env'].mpi_params is mpi_params
+
     def _get_mpi_params(self):
         """Get mpi parameters."""
         return self._mpi_params
@@ -94,8 +97,9 @@ class TopologyDescriptor(object):
         if choose_topology() returns None.
         """
         check_instance(known_topologies, list, values=Topology)
-        topo = self.choose_topology(known_topologies, **kwds) or \
-               self.create_topology(**kwds)
+        topo = self.choose_topology(known_topologies, **kwds)
+        if (topo is None):
+            topo = self.create_topology(**kwds)
         return topo
     
     @abstractmethod
diff --git a/requirements.txt b/requirements.txt
index 502b8de768a264fa733d349d0d466826316bdf42..b44b42fb480f3c6487bcd7bc3fb1cfc964d87a31 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+wheel
 numpy
 scipy
 sympy
@@ -13,8 +14,9 @@ ansicolors
 backports.weakref
 argparse_color_formatter
 primefac
+pybind11
 pyopencl
 pyfftw
-gpyfft
 mpi4py
 matplotlib
+numba
diff --git a/src/hysop++/src/fft/transform.h b/src/hysop++/src/fft/transform.h
index c300483a247472025b79a2509265090107908c8e..7a6e36a3b21f7836281adefb03b2b2c3a68dd2c7 100644
--- a/src/hysop++/src/fft/transform.h
+++ b/src/hysop++/src/fft/transform.h
@@ -70,7 +70,7 @@ namespace hysop {
                                 else if(k==N/2)
                                     kk = T(0);
                                 else 
-                                    kk = double(k)-double(N);
+                                    kk = T(k)-T(N);
                                 return std::complex<T>(T(0),T(2)*pi*kk/L);
                             }
                         case(FFTW_REDFT00):
diff --git a/test_ci.sh b/test_ci.sh
new file mode 100755
index 0000000000000000000000000000000000000000..6d4c7e65d4ee25936afe3726269f547f0cd9a022
--- /dev/null
+++ b/test_ci.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+set -e
+
+if [ -z "$HYSOP_ROOT" ]; then
+    HYSOP_ROOT=$(pwd)
+    echo "Warning: HYSOP_ROOT has not been set."
+    echo "Setting HYSOP_ROOT to '$HYSOP_ROOT'"
+fi
+
+$HYSOP_ROOT/ci/scripts/test.sh $HYSOP_ROOT $HYSOP_ROOT/hysop
+
+exit 0
diff --git a/test_examples.sh b/test_examples.sh
new file mode 100755
index 0000000000000000000000000000000000000000..4af3534c49b6d7a060e2c413c85862fde6269900
--- /dev/null
+++ b/test_examples.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+set -e
+
+if [ -z "$HYSOP_ROOT" ]; then
+    HYSOP_ROOT=$(pwd)
+    echo "Warning: HYSOP_ROOT has not been set."
+    echo "Setting HYSOP_ROOT to '$HYSOP_ROOT'"
+fi
+
+export HYSOP_VERBOSE=1
+export HYSOP_DEBUG=0
+export HYSOP_PROFILE=0
+export HYSOP_KERNEL_DEBUG=0
+python -c 'import hysop; print hysop'
+
+EXAMPLE_DIR="$HYSOP_ROOT/examples"
+EXAMPLE_OPTIONS='-cp default -maxit 2'
+python "$EXAMPLE_DIR/analytic/analytic.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/scalar_diffusion/scalar_diffusion.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/scalar_advection/scalar_advection.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/shear_layer/shear_layer.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/taylor_green/taylor_green.py" -impl python $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/taylor_green/taylor_green.py" -impl opencl $EXAMPLE_OPTIONS
+python -c "from hysop.f2hysop import scales2py as scales" && python "$EXAMPLE_DIR/taylor_green/taylor_green.py" -impl fortran $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/bubble/periodic_bubble.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/bubble/periodic_bubble_levelset.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/bubble/periodic_bubble_levelset_penalization.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/bubble/periodic_jet_levelset.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/particles_above_salt/particles_above_salt_periodic.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/particles_above_salt/particles_above_salt_symmetrized.py" $EXAMPLE_OPTIONS
+python "$EXAMPLE_DIR/particles_above_salt/particles_above_salt_bc.py" $EXAMPLE_OPTIONS
+
+exit 0