From acd6f21a1ce942c7e95252c84e8a950031fd00b5 Mon Sep 17 00:00:00 2001
From: Cristian C Lalescu <Cristian.Lalescu@ds.mpg.de>
Date: Wed, 5 Jul 2017 14:53:41 +0200
Subject: [PATCH] add box filter and filter test

---
 bfps/TEST.py                         | 300 +++++++++++++++++++++++++++
 bfps/__main__.py                     |   7 +-
 bfps/cpp/full_code/NSVEparticles.cpp |   2 +
 bfps/cpp/full_code/filter_test.cpp   | 181 ++++++++++++++++
 bfps/cpp/full_code/filter_test.hpp   |  69 ++++++
 bfps/cpp/kspace.cpp                  |  42 +++-
 bfps/cpp/kspace.hpp                  |   6 +
 setup.py                             |   3 +-
 8 files changed, 605 insertions(+), 5 deletions(-)
 create mode 100644 bfps/TEST.py
 create mode 100644 bfps/cpp/full_code/filter_test.cpp
 create mode 100644 bfps/cpp/full_code/filter_test.hpp

diff --git a/bfps/TEST.py b/bfps/TEST.py
new file mode 100644
index 00000000..90c64d30
--- /dev/null
+++ b/bfps/TEST.py
@@ -0,0 +1,300 @@
+#######################################################################
+#                                                                     #
+#  Copyright 2015 Max Planck Institute                                #
+#                 for Dynamics and Self-Organization                  #
+#                                                                     #
+#  This file is part of bfps.                                         #
+#                                                                     #
+#  bfps is free software: you can redistribute it and/or modify       #
+#  it under the terms of the GNU General Public License as published  #
+#  by the Free Software Foundation, either version 3 of the License,  #
+#  or (at your option) any later version.                             #
+#                                                                     #
+#  bfps is distributed in the hope that it will be useful,            #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of     #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the      #
+#  GNU General Public License for more details.                       #
+#                                                                     #
+#  You should have received a copy of the GNU General Public License  #
+#  along with bfps.  If not, see <http://www.gnu.org/licenses/>       #
+#                                                                     #
+# Contact: Cristian.Lalescu@ds.mpg.de                                 #
+#                                                                     #
+#######################################################################
+
+
+
+import os
+import sys
+import shutil
+import subprocess
+import argparse
+import h5py
+import math
+import numpy as np
+import warnings
+
+import bfps
+from ._code import _code
+from bfps import tools
+
+class TEST(_code):
+    """This class is meant to stitch together the C++ code into a final source file,
+    compile it, and handle all job launching.
+    """
+    def __init__(
+            self,
+            work_dir = './',
+            simname = 'test'):
+        _code.__init__(
+                self,
+                work_dir = work_dir,
+                simname = simname)
+        self.host_info = {'type'        : 'cluster',
+                          'environment' : None,
+                          'deltanprocs' : 1,
+                          'queue'       : '',
+                          'mail_address': '',
+                          'mail_events' : None}
+        self.generate_default_parameters()
+        return None
+    def set_precision(
+            self,
+            fluid_dtype):
+        if fluid_dtype in [np.float32, np.float64]:
+            self.fluid_dtype = fluid_dtype
+        elif fluid_dtype in ['single', 'double']:
+            if fluid_dtype == 'single':
+                self.fluid_dtype = np.dtype(np.float32)
+            elif fluid_dtype == 'double':
+                self.fluid_dtype = np.dtype(np.float64)
+        self.rtype = self.fluid_dtype
+        if self.rtype == np.float32:
+            self.ctype = np.dtype(np.complex64)
+            self.C_field_dtype = 'float'
+            self.fluid_precision = 'single'
+        elif self.rtype == np.float64:
+            self.ctype = np.dtype(np.complex128)
+            self.C_field_dtype = 'double'
+            self.fluid_precision = 'double'
+        return None
+    def write_src(self):
+        self.version_message = (
+                '/***********************************************************************\n' +
+                '* this code automatically generated by bfps\n' +
+                '* version {0}\n'.format(bfps.__version__) +
+                '***********************************************************************/\n\n\n')
+        self.include_list = [
+                '"base.hpp"',
+                '"scope_timer.hpp"',
+                '"fftw_interface.hpp"',
+                '"full_code/main_code.hpp"',
+                '<cmath>',
+                '<iostream>',
+                '<hdf5.h>',
+                '<string>',
+                '<cstring>',
+                '<fftw3-mpi.h>',
+                '<omp.h>',
+                '<cfenv>',
+                '<cstdlib>',
+                '"full_code/{0}.hpp"\n'.format(self.dns_type)]
+        self.main = """
+            int main(int argc, char *argv[])
+            {{
+                bool fpe = (
+                    (getenv("BFPS_FPE_OFF") == nullptr) ||
+                    (getenv("BFPS_FPE_OFF") != std::string("TRUE")));
+                return main_code< {0} >(argc, argv, fpe);
+            }}
+            """.format(self.dns_type + '<{0}>'.format(self.C_field_dtype))
+        self.includes = '\n'.join(
+                ['#include ' + hh
+                 for hh in self.include_list])
+        with open(self.name + '.cpp', 'w') as outfile:
+            outfile.write(self.version_message + '\n\n')
+            outfile.write(self.includes + '\n\n')
+            outfile.write(self.main + '\n')
+        return None
+    def generate_default_parameters(self):
+        # these parameters are relevant for all TEST classes
+        self.parameters['dealias_type'] = int(1)
+        self.parameters['dkx'] = float(1.0)
+        self.parameters['dky'] = float(1.0)
+        self.parameters['dkz'] = float(1.0)
+        self.parameters['filter_length'] = float(1.0)
+        self.parameters['niter_todo'] = int(1)
+        self.parameters['niter_stat'] = int(1)
+        self.parameters['niter_out'] = int(1)
+        self.parameters['checkpoints_per_file'] = int(1)
+        return None
+    def get_kspace(self):
+        kspace = {}
+        if self.parameters['dealias_type'] == 1:
+            kMx = self.parameters['dkx']*(self.parameters['nx']//2 - 1)
+            kMy = self.parameters['dky']*(self.parameters['ny']//2 - 1)
+            kMz = self.parameters['dkz']*(self.parameters['nz']//2 - 1)
+        else:
+            kMx = self.parameters['dkx']*(self.parameters['nx']//3 - 1)
+            kMy = self.parameters['dky']*(self.parameters['ny']//3 - 1)
+            kMz = self.parameters['dkz']*(self.parameters['nz']//3 - 1)
+        kspace['kM'] = max(kMx, kMy, kMz)
+        kspace['dk'] = min(self.parameters['dkx'],
+                           self.parameters['dky'],
+                           self.parameters['dkz'])
+        nshells = int(kspace['kM'] / kspace['dk']) + 2
+        kspace['nshell'] = np.zeros(nshells, dtype = np.int64)
+        kspace['kshell'] = np.zeros(nshells, dtype = np.float64)
+        kspace['kx'] = np.arange( 0,
+                                  self.parameters['nx']//2 + 1).astype(np.float64)*self.parameters['dkx']
+        kspace['ky'] = np.arange(-self.parameters['ny']//2 + 1,
+                                  self.parameters['ny']//2 + 1).astype(np.float64)*self.parameters['dky']
+        kspace['ky'] = np.roll(kspace['ky'], self.parameters['ny']//2+1)
+        kspace['kz'] = np.arange(-self.parameters['nz']//2 + 1,
+                                  self.parameters['nz']//2 + 1).astype(np.float64)*self.parameters['dkz']
+        kspace['kz'] = np.roll(kspace['kz'], self.parameters['nz']//2+1)
+        return kspace
+    def get_data_file_name(self):
+        return os.path.join(self.work_dir, self.simname + '.h5')
+    def get_data_file(self):
+        return h5py.File(self.get_data_file_name(), 'r')
+    def write_par(
+            self,
+            iter0 = 0,
+            particle_ic = None):
+        assert (self.parameters['niter_todo'] == 1)
+        assert (self.parameters['niter_out'] == 1)
+        assert (self.parameters['niter_stat'] == 1)
+        assert (iter0 == 0)
+        _code.write_par(self, iter0 = iter0)
+        with h5py.File(self.get_data_file_name(), 'r+') as ofile:
+            ofile['bfps_info/exec_name'] = self.name
+            kspace = self.get_kspace()
+            for k in kspace.keys():
+                ofile['kspace/' + k] = kspace[k]
+            nshells = kspace['nshell'].shape[0]
+            kspace = self.get_kspace()
+            nshells = kspace['nshell'].shape[0]
+            ofile['checkpoint'] = int(0)
+        return None
+    def job_parser_arguments(
+            self,
+            parser):
+        parser.add_argument(
+                '--ncpu',
+                type = int,
+                dest = 'ncpu',
+                default = -1)
+        parser.add_argument(
+                '--np', '--nprocesses',
+                metavar = 'NPROCESSES',
+                help = 'number of mpi processes to use',
+                type = int,
+                dest = 'nb_processes',
+                default = 4)
+        parser.add_argument(
+                '--ntpp', '--nthreads-per-process',
+                type = int,
+                dest = 'nb_threads_per_process',
+                metavar = 'NTHREADS_PER_PROCESS',
+                help = 'number of threads to use per MPI process',
+                default = 1)
+        parser.add_argument(
+                '--no-submit',
+                action = 'store_true',
+                dest = 'no_submit')
+        parser.add_argument(
+                '--environment',
+                type = str,
+                dest = 'environment',
+                default = None)
+        parser.add_argument(
+                '--minutes',
+                type = int,
+                dest = 'minutes',
+                default = 5,
+                help = 'If environment supports it, this is the requested wall-clock-limit.')
+        parser.add_argument(
+               '--njobs',
+               type = int, dest = 'njobs',
+               default = 1)
+        return None
+    def simulation_parser_arguments(
+            self,
+            parser):
+        parser.add_argument(
+                '--simname',
+                type = str, dest = 'simname',
+                default = 'test')
+        parser.add_argument(
+               '-n', '--grid-size',
+               type = int,
+               dest = 'n',
+               default = 32,
+               metavar = 'N',
+               help = 'code is run by default in a grid of NxNxN')
+        for coord in ['x', 'y', 'z']:
+            parser.add_argument(
+                   '--L{0}'.format(coord), '--box-length-{0}'.format(coord),
+                   type = float,
+                   dest = 'L{0}'.format(coord),
+                   default = 2.0,
+                   metavar = 'length{0}'.format(coord),
+                   help = 'length of the box in the {0} direction will be `length{0} x pi`'.format(coord))
+        parser.add_argument(
+                '--wd',
+                type = str, dest = 'work_dir',
+                default = './')
+        parser.add_argument(
+                '--precision',
+                choices = ['single', 'double'],
+                type = str,
+                default = 'single')
+        return None
+    def add_parser_arguments(
+            self,
+            parser):
+        subparsers = parser.add_subparsers(
+                dest = 'TEST_class',
+                help = 'type of simulation to run')
+        subparsers.required = True
+        parser_filter_test = subparsers.add_parser(
+                'filter_test',
+                help = 'plain filter test')
+        self.simulation_parser_arguments(parser_filter_test)
+        self.job_parser_arguments(parser_filter_test)
+        self.parameters_to_parser_arguments(parser_filter_test)
+        return None
+    def prepare_launch(
+            self,
+            args = []):
+        opt = _code.prepare_launch(self, args = args)
+        self.set_precision(opt.precision)
+        self.dns_type = opt.TEST_class
+        self.name = self.dns_type + '-' + self.fluid_precision + '-v' + bfps.__version__
+        # merge parameters if needed
+        self.pars_from_namespace(opt)
+        return opt
+    def launch(
+            self,
+            args = [],
+            **kwargs):
+        opt = self.prepare_launch(args = args)
+        self.launch_jobs(opt = opt, **kwargs)
+        return None
+    def launch_jobs(
+            self,
+            opt = None,
+            particle_initial_condition = None):
+        if not os.path.exists(os.path.join(self.work_dir, self.simname + '.h5')):
+            self.write_par(
+                    particle_ic = None)
+        self.run(
+                nb_processes = opt.nb_processes,
+                nb_threads_per_process = opt.nb_threads_per_process,
+                njobs = opt.njobs,
+                hours = opt.minutes // 60,
+                minutes = opt.minutes % 60,
+                no_submit = opt.no_submit)
+        return None
+
diff --git a/bfps/__main__.py b/bfps/__main__.py
index fa30289b..c41a6ffb 100644
--- a/bfps/__main__.py
+++ b/bfps/__main__.py
@@ -30,6 +30,7 @@ import argparse
 import bfps
 from .DNS import DNS
 from .PP import PP
+from .TEST import TEST
 from .NavierStokes import NavierStokes
 from .NSVorticityEquation import NSVorticityEquation
 from .FluidResize import FluidResize
@@ -66,7 +67,7 @@ def main():
                'NSManyParticles-double']
     parser.add_argument(
             'base_class',
-            choices = ['DNS', 'PP'] +
+            choices = ['DNS', 'PP', 'TEST'] +
                       NSoptions +
                       NSVEoptions +
                       FRoptions +
@@ -86,6 +87,10 @@ def main():
         c = PP()
         c.launch(args = sys.argv[2:])
         return None
+    if opt.base_class == 'TEST':
+        c = TEST()
+        c.launch(args = sys.argv[2:])
+        return None
     if 'double' in opt.base_class:
         precision = 'double'
     else:
diff --git a/bfps/cpp/full_code/NSVEparticles.cpp b/bfps/cpp/full_code/NSVEparticles.cpp
index ba84b394..cb9d17da 100644
--- a/bfps/cpp/full_code/NSVEparticles.cpp
+++ b/bfps/cpp/full_code/NSVEparticles.cpp
@@ -84,6 +84,7 @@ int NSVEparticles<rnumber>::do_stats()
                                  "tracers0",                        // hdf5 parent group
                                  "velocity"                         // dataset basename TODO
                                  );
+    DEBUG_MSG("hello after sampling velocity\n");
 
     /// compute acceleration and sample it
     this->fs->compute_Lagrangian_acceleration(this->tmp_vec_field);
@@ -93,6 +94,7 @@ int NSVEparticles<rnumber>::do_stats()
                                  (this->simname + "_particles.h5"),
                                  "tracers0",
                                  "acceleration");
+    DEBUG_MSG("hello after sampling acceleration\n");
 
     return EXIT_SUCCESS;
 }
diff --git a/bfps/cpp/full_code/filter_test.cpp b/bfps/cpp/full_code/filter_test.cpp
new file mode 100644
index 00000000..6ed16373
--- /dev/null
+++ b/bfps/cpp/full_code/filter_test.cpp
@@ -0,0 +1,181 @@
+#include <string>
+#include <cmath>
+#include "filter_test.hpp"
+#include "scope_timer.hpp"
+
+
+template <typename rnumber>
+int filter_test<rnumber>::initialize(void)
+{
+    this->read_parameters();
+    this->scal_field = new field<rnumber, FFTW, ONE>(
+            nx, ny, nz,
+            this->comm,
+            DEFAULT_FFTW_FLAG);
+    this->kk = new kspace<FFTW, SMOOTH>(
+            this->scal_field->clayout, this->dkx, this->dky, this->dkz);
+
+    if (this->myrank == 0)
+    {
+        hid_t stat_file = H5Fopen(
+                (this->simname + std::string(".h5")).c_str(),
+                H5F_ACC_RDWR,
+                H5P_DEFAULT);
+        this->kk->store(stat_file);
+        H5Fclose(stat_file);
+    }
+    this->read_iteration();
+    return EXIT_SUCCESS;
+}
+
+template <typename rnumber>
+int filter_test<rnumber>::reset_field(
+        int dimension)
+{
+    this->scal_field->real_space_representation = true;
+    *this->scal_field = 0.0;
+    if (this->scal_field->rlayout->starts[0] == 0)
+    {
+        switch(dimension)
+        {
+            case 0:
+                this->scal_field->rval(0) = 1.0 / (
+                        (4*acos(0) / (this->nx*this->dkx))*
+                        (4*acos(0) / (this->ny*this->dky))*
+                        (4*acos(0) / (this->nz*this->dkz)));
+                break;
+            case 1:
+                for (ptrdiff_t xindex = 0; xindex < this->nx; xindex++)
+                    this->scal_field->rval(this->scal_field->get_rindex(xindex, 0, 0)) = 1.0 / (
+                        (4*acos(0) / (this->ny*this->dky))*
+                        (4*acos(0) / (this->nz*this->dkz)));
+                break;
+            case 2:
+                for (ptrdiff_t yindex = 0; yindex < this->ny; yindex++)
+                for (ptrdiff_t xindex = 0; xindex < this->nx; xindex++)
+                {
+                    this->scal_field->rval(
+                            this->scal_field->get_rindex(xindex, yindex, 0)) = 1.0 / (
+                        (4*acos(0) / (this->nz*this->dkz)));
+                }
+                break;
+            default:
+                break;
+        }
+    }
+    this->scal_field->dft();
+    this->scal_field->symmetrize();
+    return EXIT_SUCCESS;
+}
+
+template <typename rnumber>
+int filter_test<rnumber>::step(void)
+{
+    this->iteration = (this->iteration + this->niter_todo -
+                       (this->iteration % this->niter_todo));
+    return EXIT_SUCCESS;
+}
+
+template <typename rnumber>
+int filter_test<rnumber>::write_checkpoint(void)
+{
+    std::string filename = this->simname + std::string("_fields.h5");
+    for (int dimension = 0; dimension < 3; dimension++)
+    {
+        this->reset_field(dimension);
+        this->kk->template filter<rnumber, ONE>(
+                this->scal_field->get_cdata(),
+                4*acos(0) / (this->filter_length),
+                "sharp_Fourier_sphere");
+        this->scal_field->ift();
+        this->scal_field->normalize();
+        this->scal_field->io(
+                filename,
+                "sharp_Fourier_sphere",
+                dimension,
+                false);
+        this->reset_field(dimension);
+        this->kk->template filter<rnumber, ONE>(
+                this->scal_field->get_cdata(),
+                4*acos(0) / (this->filter_length / sqrt(log(2))),
+                "Gauss");
+        this->scal_field->ift();
+        this->scal_field->normalize();
+        this->scal_field->io(
+                filename,
+                "Gauss",
+                dimension,
+                false);
+        this->reset_field(dimension);
+        this->kk->template filter<rnumber, ONE>(
+                this->scal_field->get_cdata(),
+                4*acos(0) / (this->filter_length*2),
+                "ball");
+        this->scal_field->ift();
+        this->scal_field->normalize();
+        this->scal_field->io(
+                filename,
+                "ball",
+                dimension,
+                false);
+    }
+    return EXIT_SUCCESS;
+}
+
+template <typename rnumber>
+int filter_test<rnumber>::finalize(void)
+{
+    delete this->scal_field;
+    delete this->kk;
+    return EXIT_SUCCESS;
+}
+
+template <typename rnumber>
+int filter_test<rnumber>::read_parameters()
+{
+    hid_t parameter_file;
+    hid_t dset, memtype, space;
+    char fname[256];
+    char *string_data;
+    sprintf(fname, "%s.h5", this->simname.c_str());
+    parameter_file = H5Fopen(fname, H5F_ACC_RDONLY, H5P_DEFAULT);
+    dset = H5Dopen(parameter_file, "/parameters/niter_todo", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->niter_todo);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/niter_stat", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->niter_stat);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/niter_out", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->niter_out);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/dealias_type", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->dealias_type);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/dkx", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->dkx);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/dky", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->dky);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/dkz", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->dkz);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/nx", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->nx);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/ny", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->ny);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/nz", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_INT, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->nz);
+    H5Dclose(dset);
+    dset = H5Dopen(parameter_file, "/parameters/filter_length", H5P_DEFAULT);
+    H5Dread(dset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, &this->filter_length);
+    H5Dclose(dset);
+    H5Fclose(parameter_file);
+    return EXIT_SUCCESS;
+}
+
+template class filter_test<float>;
+template class filter_test<double>;
+
diff --git a/bfps/cpp/full_code/filter_test.hpp b/bfps/cpp/full_code/filter_test.hpp
new file mode 100644
index 00000000..83ea60e3
--- /dev/null
+++ b/bfps/cpp/full_code/filter_test.hpp
@@ -0,0 +1,69 @@
+/**********************************************************************
+*                                                                     *
+*  Copyright 2017 Max Planck Institute                                *
+*                 for Dynamics and Self-Organization                  *
+*                                                                     *
+*  This file is part of bfps.                                         *
+*                                                                     *
+*  bfps is free software: you can redistribute it and/or modify       *
+*  it under the terms of the GNU General Public License as published  *
+*  by the Free Software Foundation, either version 3 of the License,  *
+*  or (at your option) any later version.                             *
+*                                                                     *
+*  bfps is distributed in the hope that it will be useful,            *
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of     *
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the      *
+*  GNU General Public License for more details.                       *
+*                                                                     *
+*  You should have received a copy of the GNU General Public License  *
+*  along with bfps.  If not, see <http://www.gnu.org/licenses/>       *
+*                                                                     *
+* Contact: Cristian.Lalescu@ds.mpg.de                                 *
+*                                                                     *
+**********************************************************************/
+
+
+
+#ifndef FILTER_TEST_HPP
+#define FILTER_TEST_HPP
+
+
+
+#include <cstdlib>
+#include "base.hpp"
+#include "vorticity_equation.hpp"
+#include "full_code/direct_numerical_simulation.hpp"
+
+template <typename rnumber>
+class filter_test: public direct_numerical_simulation
+{
+    public:
+
+        /* parameters that are read in read_parameters */
+        double filter_length;
+
+        /* other stuff */
+        kspace<FFTW, SMOOTH> *kk;
+        field<rnumber, FFTW, ONE> *scal_field;
+
+        filter_test(
+                const MPI_Comm COMMUNICATOR,
+                const std::string &simulation_name):
+            direct_numerical_simulation(
+                    COMMUNICATOR,
+                    simulation_name){}
+        ~filter_test(){}
+
+        int initialize(void);
+        int step(void);
+        int finalize(void);
+
+        int reset_field(int dimension);
+
+        int read_parameters(void);
+        int write_checkpoint(void);
+        int do_stats(void){}
+};
+
+#endif//FILTER_TEST_HPP
+
diff --git a/bfps/cpp/kspace.cpp b/bfps/cpp/kspace.cpp
index 96425fd2..4c296fa2 100644
--- a/bfps/cpp/kspace.cpp
+++ b/bfps/cpp/kspace.cpp
@@ -252,6 +252,37 @@ void kspace<be, dt>::low_pass(
                 });
 }
 
+/** \brief Filter a field using a ball shaped top hat filter.
+ *
+ *  Filter's mathematical expression in Fourier space is as follows:
+ *  \f[
+ *      \hat{b}_\ell(\mathbf{k}) = \sin(k \ell / 2) / (k \ell / 2)
+ *  \f]
+ */
+template <field_backend be,
+          kspace_dealias_type dt>
+template <typename rnumber,
+          field_components fc>
+void kspace<be, dt>::ball_filter(
+        typename fftw_interface<rnumber>::complex *__restrict__ a,
+        const double sigma)
+{
+    const double prefactor = sigma/2;
+    this->CLOOP_K2(
+            [&](ptrdiff_t cindex,
+                ptrdiff_t xindex,
+                ptrdiff_t yindex,
+                ptrdiff_t zindex,
+                double k2){
+                if (k2 > 0)
+                {
+                    double argument = sqrt(k2)*prefactor;
+                    for (unsigned int tcounter=0; tcounter<2*ncomp(fc); tcounter++)
+                        ((rnumber*)a)[2*ncomp(fc)*cindex + tcounter] *= sin(argument) / argument;
+                }
+                });
+}
+
 /** \brief Filter a field using a Gaussian kernel.
  *
  *  Filter's mathematical expression in Fourier space is as follows:
@@ -274,7 +305,6 @@ void kspace<be, dt>::Gauss_filter(
                 ptrdiff_t yindex,
                 ptrdiff_t zindex,
                 double k2){
-                if (k2 <= this->kM2)
                 {
                     for (unsigned int tcounter=0; tcounter<2*ncomp(fc); tcounter++)
                         ((rnumber*)a)[2*ncomp(fc)*cindex + tcounter] *= exp(prefactor*k2);
@@ -284,8 +314,8 @@ void kspace<be, dt>::Gauss_filter(
 
 /** \brief Filter a field.
  *
- *  This is a wrapper that can choose between a sharp Fourier spherical filter
- *  and a Gaussian filter.
+ *  This is a wrapper that can choose between a sharp Fourier spherical filter,
+ *  a Gaussian filter and a sharp real space spherical filter.
  *  The cutoff wavenumber \f$k_c\f$ is a parameter, so the low pass Fourier
  *  operation is straightforward.
  *
@@ -318,6 +348,12 @@ int kspace<be, dt>::filter(
                 a,
                 2*acos(0.)/wavenumber);
     }
+    else if (filter_type == std::string("ball"))
+    {
+        this->template ball_filter<rnumber, fc>(
+                a,
+                2*acos(0.)/wavenumber);
+    }
     return EXIT_SUCCESS;
 }
 
diff --git a/bfps/cpp/kspace.hpp b/bfps/cpp/kspace.hpp
index 7fa77e8b..eebec838 100644
--- a/bfps/cpp/kspace.hpp
+++ b/bfps/cpp/kspace.hpp
@@ -82,6 +82,12 @@ class kspace
                 typename fftw_interface<rnumber>::complex *__restrict__ a,
                 const double sigma);
 
+        template <typename rnumber,
+                  field_components fc>
+        void ball_filter(
+                typename fftw_interface<rnumber>::complex *__restrict__ a,
+                const double sigma);
+
         template <typename rnumber,
                   field_components fc>
         int filter(
diff --git a/setup.py b/setup.py
index 1178f547..8562d375 100644
--- a/setup.py
+++ b/setup.py
@@ -88,7 +88,8 @@ print('This is bfps version ' + VERSION)
 
 
 ### lists of files and MANIFEST.in
-src_file_list = ['hdf5_tools',
+src_file_list = ['full_code/filter_test',
+                 'hdf5_tools',
                  'full_code/get_rfields',
                  'full_code/NSVE_field_stats',
                  'full_code/native_binary_to_hdf5',
-- 
GitLab