Przeglądaj źródła

r.mfilter: implement parallelization with OpenMP (#1708)

Co-authored-by: Anna Petrasova <kratochanna@gmail.com>
Aaron Saw Min Sern 3 lat temu
rodzic
commit
829768dcfc

+ 2 - 1
raster/r.mfilter/Makefile

@@ -2,8 +2,9 @@ MODULE_TOPDIR = ../..
 
 
 PGM = r.mfilter
 PGM = r.mfilter
 
 
-LIBES = $(ROWIOLIB) $(RASTERLIB) $(GISLIB)
+LIBES = $(ROWIOLIB) $(RASTERLIB) $(GISLIB) $(OMPLIB)
 DEPENDENCIES = $(ROWIODEP) $(RASTERDEP) $(GISDEP)
 DEPENDENCIES = $(ROWIODEP) $(RASTERDEP) $(GISDEP)
+EXTRA_CFLAGS = $(OMPCFLAGS)
 
 
 include $(MODULE_TOPDIR)/include/Make/Module.make
 include $(MODULE_TOPDIR)/include/Make/Module.make
 
 

+ 73 - 0
raster/r.mfilter/benchmark/benchmark_r_mfilter_nprocs.py

@@ -0,0 +1,73 @@
+"""Benchmarking of r.mfilter
+
+@author Aaron Saw Min Sern
+"""
+
+from grass.exceptions import CalledModuleError
+from grass.pygrass.modules import Module
+from grass.script import tempfile
+from subprocess import DEVNULL
+
+import grass.benchmark as bm
+
+
+def main():
+    results = []
+
+    # Users can add more or modify existing reference maps
+    benchmark(7071, "r.mfilter_50M", results)
+    benchmark(10000, "r.mfilter_100M", results)
+    benchmark(14142, "r.mfilter_200M", results)
+    benchmark(20000, "r.mfilter_400M", results)
+
+    bm.nprocs_plot(results)
+
+
+def benchmark(size, label, results):
+    reference = "r_mfilter_reference_map"
+    output = "benchmark_r_mfilter_nprocs"
+    filter = tempfile()
+    with open(filter, "w") as w:
+        w.write(
+            """MATRIX 9
+                   1 1 1 1 1 1 1 1 1
+                   1 2 1 2 1 2 1 2 1
+                   1 1 3 1 3 1 3 1 1
+                   1 2 1 4 1 4 1 2 1
+                   1 1 3 1 5 1 3 1 1
+                   1 2 1 4 1 4 1 2 1
+                   1 1 3 1 3 1 3 1 1
+                   1 2 1 2 1 2 1 2 1
+                   1 1 1 1 1 1 1 1 1
+                   DIVISOR 81
+                   TYPE    P"""
+        )
+
+    generate_map(rows=size, cols=size, fname=reference)
+    module = Module(
+        "r.mfilter",
+        input=reference,
+        output=output,
+        filter=filter,
+        run_=False,
+        stdout_=DEVNULL,
+        overwrite=True,
+    )
+    results.append(bm.benchmark_nprocs(module, label=label, max_nprocs=16, repeat=3))
+    Module("g.remove", quiet=True, flags="f", type="raster", name=reference)
+    Module("g.remove", quiet=True, flags="f", type="raster", name=output)
+
+
+def generate_map(rows, cols, fname):
+    Module("g.region", flags="p", s=0, n=rows, w=0, e=cols, res=1)
+    # Generate using r.random.surface if r.surf.fractal fails
+    try:
+        print("Generating reference map using r.surf.fractal...")
+        Module("r.surf.fractal", output=fname)
+    except CalledModuleError:
+        print("r.surf.fractal fails, using r.random.surface instead...")
+        Module("r.random.surface", output=fname)
+
+
+if __name__ == "__main__":
+    main()

+ 75 - 29
raster/r.mfilter/execute.c

@@ -1,12 +1,17 @@
+#if defined(_OPENMP)
+    #include <omp.h>
+#endif
+
 #include <unistd.h>
 #include <unistd.h>
 #include <grass/rowio.h>
 #include <grass/rowio.h>
 #include <grass/raster.h>
 #include <grass/raster.h>
 #include "glob.h"
 #include "glob.h"
 #include "filter.h"
 #include "filter.h"
 
 
-int execute_filter(ROWIO * r, int out, FILTER * filter, DCELL * cell)
+int execute_filter(ROWIO *r, int *out, FILTER *filter, DCELL **cell)
 {
 {
     int i;
     int i;
+    int t;
     int count;
     int count;
     int size;
     int size;
     int row, rcount;
     int row, rcount;
@@ -14,12 +19,25 @@ int execute_filter(ROWIO * r, int out, FILTER * filter, DCELL * cell)
     int startx, starty;
     int startx, starty;
     int dx, dy;
     int dx, dy;
     int mid;
     int mid;
-    DCELL **bufs, **box, *cp;
+    int old_nprocs = 0;
+    DCELL ***bufs, ***box, *cp;
+
+    if (nprocs > 1 && filter->type == SEQUENTIAL) {
+        /* disable parallel temporarily */
+        old_nprocs = nprocs;
+        nprocs = 1;
+    }
+
 
 
     size = filter->size;
     size = filter->size;
     mid = size / 2;
     mid = size / 2;
-    bufs = (DCELL **) G_malloc(size * sizeof(DCELL *));
-    box = (DCELL **) G_malloc(size * sizeof(DCELL *));
+    bufs = (DCELL ***) G_malloc(nprocs * sizeof(DCELL **));
+    box = (DCELL ***) G_malloc(nprocs * sizeof(DCELL **));
+
+    for (t = 0; t < nprocs; t++) {
+        bufs[t] = (DCELL **) G_malloc(size * sizeof(DCELL *));
+        box[t] = (DCELL **) G_malloc(size * sizeof(DCELL *));
+    }
 
 
     switch (filter->start) {
     switch (filter->start) {
     case UR:
     case UR:
@@ -56,66 +74,94 @@ int execute_filter(ROWIO * r, int out, FILTER * filter, DCELL * cell)
     ccount = ncols - (size - 1);
     ccount = ncols - (size - 1);
 
 
     /* rewind output */
     /* rewind output */
-    lseek(out, 0L, 0);
+    lseek(out[MASTER], 0L, SEEK_SET);
 
 
     /* copy border rows to output */
     /* copy border rows to output */
     row = starty;
     row = starty;
     for (i = 0; i < mid; i++) {
     for (i = 0; i < mid; i++) {
-	cp = (DCELL *) Rowio_get(r, row);
-	write(out, cp, buflen);
-	row += dy;
+        cp = (DCELL *) Rowio_get(&r[MASTER], row);
+        if (write(out[MASTER], cp, buflen) < 0) 
+            G_fatal_error("Error writing temporary file");
+        row += dy;
     }
     }
 
 
     /* for each row */
     /* for each row */
-    for (count = 0; count < rcount; count++) {
-	G_percent(count, rcount, 2);
+    int id = MASTER;
+    int start =  0;
+    int end = rcount;
+    int work = 0;
+    DCELL* cellp = cell[MASTER];
+
+    #pragma omp parallel firstprivate(starty, id, start, end, cellp) private(i, count, row, col, cp) if(nprocs > 1)
+    {
+    #if defined(_OPENMP)
+    if (nprocs > 1) {
+        id = omp_get_thread_num();
+        start = rcount * id/nprocs;
+        end = rcount * (id+1)/nprocs;
+        cellp = cell[id];
+        starty += start * dy;
+        lseek(out[id], (off_t) buflen * (mid + start), SEEK_SET);
+    }
+    #endif
+
+    for (count = start; count < end; count++) {
+	G_percent(work, rcount, 2);
 	row = starty;
 	row = starty;
 	starty += dy;
 	starty += dy;
 	/* get "size" rows */
 	/* get "size" rows */
 	for (i = 0; i < size; i++) {
 	for (i = 0; i < size; i++) {
-	    bufs[i] = (DCELL *) Rowio_get(r, row);
-	    box[i] = bufs[i] + startx;
+	    bufs[id][i] = (DCELL *) Rowio_get(&r[id], row);
+	    box[id][i] = bufs[id][i] + startx;
 	    row += dy;
 	    row += dy;
 	}
 	}
 	if (filter->type == SEQUENTIAL)
 	if (filter->type == SEQUENTIAL)
-	    cell = bufs[mid];
+	    cellp = bufs[id][mid];
 	/* copy border */
 	/* copy border */
-	cp = cell;
+	cp = cellp;
 	for (i = 0; i < mid; i++)
 	for (i = 0; i < mid; i++)
-	    *cp++ = bufs[mid][i];
+	    *cp++ = bufs[id][mid][i];
 
 
 	/* filter row */
 	/* filter row */
 	col = ccount;
 	col = ccount;
 	while (col--) {
 	while (col--) {
-	    if (null_only) {
-		if (Rast_is_d_null_value(&box[mid][mid]))
-		    *cp++ = apply_filter(filter, box);
-		else
-		    *cp++ = box[mid][mid];
-	    }
-	    else {
-		*cp++ = apply_filter(filter, box);
+	    if (null_only && !Rast_is_d_null_value(&box[id][mid][mid])) {
+		    *cp++ = box[id][mid][mid];
+	    } else {
+            *cp++ = apply_filter(filter, box[id]);
 	    }
 	    }
 	    for (i = 0; i < size; i++)
 	    for (i = 0; i < size; i++)
-		box[i] += dx;
+		box[id][i] += dx;
 	}
 	}
 
 
 	/* copy border */
 	/* copy border */
 	for (i = ncols - mid; i < ncols; i++)
 	for (i = ncols - mid; i < ncols; i++)
-	    *cp++ = bufs[mid][i];
+	    *cp++ = bufs[id][mid][i];
 
 
 	/* write row */
 	/* write row */
-	write(out, cell, buflen);
+	write(out[id], cellp, buflen);
+    #pragma omp atomic update
+    work++;
     }
     }
-    G_percent(count, rcount, 2);
+    }
+    G_percent(work, rcount, 2);
+    starty = rcount * dy;
+    lseek(out[MASTER], (off_t) buflen * (mid + rcount), SEEK_SET);
 
 
     /* copy border rows to output */
     /* copy border rows to output */
     row = starty + mid * dy;
     row = starty + mid * dy;
     for (i = 0; i < mid; i++) {
     for (i = 0; i < mid; i++) {
-	cp = (DCELL *) Rowio_get(r, row);
-	write(out, cp, buflen);
+	cp = (DCELL *) Rowio_get(&r[MASTER], row);
+        if (write(out[MASTER], cp, buflen) < 0)
+            G_fatal_error("Error writing temporary file");
 	row += dy;
 	row += dy;
     }
     }
 
 
+    if (old_nprocs != 0) {
+        /* restore parallel execution */
+        nprocs = old_nprocs;
+        old_nprocs = 0;
+    }
+
     return 0;
     return 0;
 }
 }

+ 1 - 1
raster/r.mfilter/filter.h

@@ -28,4 +28,4 @@ FILTER *get_filter(char *, int *, char *);
 int perform_filter(const char *, const char *, FILTER *, int, int);
 int perform_filter(const char *, const char *, FILTER *, int, int);
 
 
 /* execute.c */
 /* execute.c */
-int execute_filter(ROWIO *, int, FILTER *, DCELL *);
+int execute_filter(ROWIO *, int *, FILTER *, DCELL **);

+ 2 - 1
raster/r.mfilter/glob.h

@@ -1,5 +1,6 @@
-
+extern const int MASTER;
 extern int nrows, ncols;
 extern int nrows, ncols;
+extern int nprocs;
 extern int buflen;
 extern int buflen;
 extern int direction;
 extern int direction;
 extern int null_only;
 extern int null_only;

+ 28 - 3
raster/r.mfilter/main.c

@@ -5,15 +5,19 @@
  * AUTHOR(S):    Michael Shapiro, CERL (original contributor)
  * AUTHOR(S):    Michael Shapiro, CERL (original contributor)
  *               Roberto Flor <flor itc.it>, Markus Neteler <neteler itc.it>
  *               Roberto Flor <flor itc.it>, Markus Neteler <neteler itc.it>
  *               Glynn Clements <glynn gclements.plus.com>, Jachym Cepicky <jachym les-ejk.cz>,
  *               Glynn Clements <glynn gclements.plus.com>, Jachym Cepicky <jachym les-ejk.cz>,
- *               Jan-Oliver Wagner <jan intevation.de>
- * PURPOSE:      
- * COPYRIGHT:    (C) 1999-2006, 2010 by the GRASS Development Team
+ *               Jan-Oliver Wagner <jan intevation.de>,
+ *               Aaron Saw Min Sern
+ * PURPOSE:      Performs raster map matrix filter
+ * COPYRIGHT:    (C) 1999-2022 by the GRASS Development Team
  *
  *
  *               This program is free software under the GNU General Public
  *               This program is free software under the GNU General Public
  *               License (>=v2). Read the file COPYING that comes with GRASS
  *               License (>=v2). Read the file COPYING that comes with GRASS
  *               for details.
  *               for details.
  *
  *
  *****************************************************************************/
  *****************************************************************************/
+#if defined(_OPENMP)
+#include <omp.h>
+#endif
 
 
 #include <stdlib.h>
 #include <stdlib.h>
 #include <string.h>
 #include <string.h>
@@ -25,7 +29,9 @@
 #include "filter.h"
 #include "filter.h"
 #include "glob.h"
 #include "glob.h"
 
 
+const int MASTER = 0;
 int nrows, ncols;
 int nrows, ncols;
+int nprocs;
 int buflen;
 int buflen;
 int direction;
 int direction;
 int null_only;
 int null_only;
@@ -49,6 +55,7 @@ int main(int argc, char **argv)
     struct Option *opt3;
     struct Option *opt3;
     struct Option *opt4;
     struct Option *opt4;
     struct Option *opt5;
     struct Option *opt5;
+    struct Option *opt6;
 
 
     G_gisinit(argv[0]);
     G_gisinit(argv[0]);
 
 
@@ -57,6 +64,8 @@ int main(int argc, char **argv)
     G_add_keyword(_("algebra"));
     G_add_keyword(_("algebra"));
     G_add_keyword(_("statistics"));
     G_add_keyword(_("statistics"));
     G_add_keyword(_("filter"));
     G_add_keyword(_("filter"));
+    G_add_keyword(_("parallel"));
+
     module->description = _("Performs raster map matrix filter.");
     module->description = _("Performs raster map matrix filter.");
 
 
     /* Define the different options */
     /* Define the different options */
@@ -85,6 +94,8 @@ int main(int argc, char **argv)
     opt5->required = NO;
     opt5->required = NO;
     opt5->description = _("Output raster map title");
     opt5->description = _("Output raster map title");
 
 
+    opt6 = G_define_standard_option(G_OPT_M_NPROCS);
+
     /* Define the different flags */
     /* Define the different flags */
 
 
     /* this isn't implemented at all 
     /* this isn't implemented at all 
@@ -107,6 +118,20 @@ int main(int argc, char **argv)
     null_only = flag2->answer;
     null_only = flag2->answer;
 
 
     sscanf(opt4->answer, "%d", &repeat);
     sscanf(opt4->answer, "%d", &repeat);
+    sscanf(opt6->answer, "%d", &nprocs);
+    if (nprocs < 1)
+    {
+      G_fatal_error(_("<%d> is not valid number of threads."), nprocs);
+    }
+#if defined(_OPENMP)
+    omp_set_num_threads(nprocs);
+#else
+    if (nprocs != 1)
+        G_warning(_("GRASS is compiled without OpenMP support. Ignoring "
+                    "threads setting."));
+    nprocs = 1;
+#endif
+
     out_name = opt2->answer;
     out_name = opt2->answer;
     filt_name = opt3->answer;
     filt_name = opt3->answer;
 
 

+ 62 - 37
raster/r.mfilter/perform.c

@@ -10,18 +10,25 @@
 int perform_filter(const char *in_name, const char *out_name,
 int perform_filter(const char *in_name, const char *out_name,
 		   FILTER * filter, int nfilters, int repeat)
 		   FILTER * filter, int nfilters, int repeat)
 {
 {
-    int in;
-    int out;
+    int *in;
+    int *out;
     int n;
     int n;
     int pass;
     int pass;
-    ROWIO r;
+    ROWIO *r;
     char *tmp1, *tmp2;
     char *tmp1, *tmp2;
     int count;
     int count;
     int row;
     int row;
-    DCELL *cell;
+    int t;
+    DCELL **cell;
 
 
+    cell = G_malloc(nprocs * sizeof(DCELL*));
+    for (t = 0; t < nprocs; t++) {
+        cell[t] = Rast_allocate_d_buf();
+    }
 
 
-    cell = Rast_allocate_d_buf();
+    in = G_malloc(nprocs * sizeof(int));
+    out = G_malloc(nprocs * sizeof(int));
+    r = G_malloc(nprocs * sizeof(ROWIO));
 
 
     count = 0;
     count = 0;
     for (pass = 0; pass < repeat; pass++) {
     for (pass = 0; pass < repeat; pass++) {
@@ -30,58 +37,76 @@ int perform_filter(const char *in_name, const char *out_name,
 	    G_debug(1, "Filter %d", n + 1);
 	    G_debug(1, "Filter %d", n + 1);
 
 
 	    if (count == 0) {
 	    if (count == 0) {
-		in = Rast_open_old(in_name, "");
-
-		G_debug(1, "Open raster map %s = %d", in_name, in);
-
-		close(creat(tmp1 = G_tempfile(), 0666));
-		out = open(tmp1, 2);
-		if (out < 0)
-		    G_fatal_error(_("Unable to create temporary file"));
+            for (t = 0; t < nprocs; t++) {
+                in[t] = Rast_open_old(in_name, "");
+
+                G_debug(1, "Open raster map %s = %d", in_name, in[t]);
+            }
+            close(creat(tmp1 = G_tempfile(), 0666));
+
+            for (t = 0; t < nprocs; t++) {
+                out[t] = open(tmp1, 2);
+                if (out[t] < 0) {
+                    G_fatal_error(_("Unable to create temporary file"));
+                }
+            }
 	    }
 	    }
 	    else if (count == 1) {
 	    else if (count == 1) {
 
 
-		G_debug(1, "Closing raster map");
-
-		Rast_close(in);
-		in = out;
-		close(creat(tmp2 = G_tempfile(), 0666));
-		out = open(tmp2, 2);
-		if (out < 0)
-		    G_fatal_error(_("Unable to create temporary file"));
+            G_debug(1, "Closing raster map");
+            for (t = 0; t < nprocs; t++) {
+                Rast_close(in[t]);
+                in[t] = out[t];
+            }
+            close(creat(tmp2 = G_tempfile(), 0666));
+
+            for (t = 0; t < nprocs; t++) {
+                out[t] = open(tmp2, 2);
+                if (out[t] < 0) {
+                    G_fatal_error(_("Unable to create temporary file"));
+                }
+            }
 	    }
 	    }
 	    else {
 	    else {
-		int fd;
+            int fd;
 
 
-		G_debug(1, "Swap temp files");
+            G_debug(1, "Swap temp files");
 
 
-		fd = in;
-		in = out;
-		out = fd;
+            for (t = 0; t < nprocs; t++) {
+                fd = in[t];
+                in[t] = out[t];
+                out[t] = fd;
+            }
 	    }
 	    }
 
 
-	    Rowio_setup(&r, in, filter[n].size, buflen,
-			count ? getrow : getmaprow, NULL);
+        for (t = 0; t < nprocs; t++) {
+            Rowio_setup(&r[t], in[t], filter[n].size, buflen,
+                count ? getrow : getmaprow, NULL);
+        }
 
 
-	    execute_filter(&r, out, &filter[n], cell);
+        execute_filter(r, out, &filter[n], cell);
 
 
-	    Rowio_release(&r);
+        for (t = 0; t < nprocs; t++) {
+            Rowio_release(&r[t]);
+        }
 	}
 	}
     }
     }
 
 
     if (count == 1)
     if (count == 1)
-	Rast_close(in);
+    for (t = 0; t < nprocs; t++)
+        Rast_close(in[t]);
     else if (count > 1)
     else if (count > 1)
-	close(in);
+    for (t = 0; t < nprocs; t++)
+        close(in[t]);
 
 
     /* copy final result to output raster map */
     /* copy final result to output raster map */
-    in = out;
-    out = Rast_open_fp_new(out_name);
+    in[MASTER] = out[MASTER];
+    out[MASTER] = Rast_open_fp_new(out_name);
 
 
     G_message(_("Writing raster map <%s>"), out_name);
     G_message(_("Writing raster map <%s>"), out_name);
     for (row = 0; row < nrows; row++) {
     for (row = 0; row < nrows; row++) {
-	getrow(in, cell, row, buflen);
-	Rast_put_d_row(out, cell);
+        getrow(in[MASTER], cell[MASTER], row, buflen);
+        Rast_put_d_row(out[MASTER], cell[MASTER]);
     }
     }
 
 
     /* remove the temporary files before closing so that the Rast_close()
     /* remove the temporary files before closing so that the Rast_close()
@@ -91,7 +116,7 @@ int perform_filter(const char *in_name, const char *out_name,
 	unlink(tmp1);
 	unlink(tmp1);
     if (count > 1)
     if (count > 1)
 	unlink(tmp2);
 	unlink(tmp2);
-    Rast_close(out);
+    Rast_close(out[MASTER]);
 
 
     return 0;
     return 0;
 }
 }

+ 26 - 5
raster/r.mfilter/r.mfilter.html

@@ -22,7 +22,7 @@ below, under FILTERS.
 The <b>repeat</b> parameter defines the number of times the <em>filter</em>
 The <b>repeat</b> parameter defines the number of times the <em>filter</em>
 is to be applied to the <em>input</em> data.
 is to be applied to the <em>input</em> data.
 
 
-<h2>FILTERS</h2>
+<h3>FILTERS</h3>
 
 
 The <em>filter</em> file is a normal UNIX ASCII file designed by the user.
 The <em>filter</em> file is a normal UNIX ASCII file designed by the user.
 It has the following format:
 It has the following format:
@@ -83,7 +83,7 @@ For example, the following describes two filters:
 </dl>
 </dl>
 
 
 
 
-<h2>EXAMPLE FILTER FILE</h2>
+<h3>EXAMPLE FILTER FILE</h3>
 
 
 <div class="code"><pre>
 <div class="code"><pre>
       TITLE     3x3 average, non-null data only, followed by 5x5 average
       TITLE     3x3 average, non-null data only, followed by 5x5 average
@@ -104,7 +104,7 @@ For example, the following describes two filters:
      TYPE      P
      TYPE      P
 </pre></div>
 </pre></div>
 
 
-<h2>HOW THE FILTER WORKS</h2>
+<h3>HOW THE FILTER WORKS</h3>
 
 
 The filter process produces a new category value for each cell
 The filter process produces a new category value for each cell
 in the input raster map layer by multiplying the category values of the
 in the input raster map layer by multiplying the category values of the
@@ -126,6 +126,25 @@ then the next filter is applied to the intermediate result to
 produce another intermediate result;  and so on, until the
 produce another intermediate result;  and so on, until the
 final filter is applied.  Then the output cell is written.
 final filter is applied.  Then the output cell is written.
 
 
+
+<h3>PERFORMANCE</h3>
+<p>By specifying the number of parallel processes with <b>nprocs</b> option,
+<em>r.mfilter</em> can run significantly faster, see benchmarks below.
+
+<div align="center" style="margin: 10px">
+     <img src="r_mfilter_benchmark_1.png" alt="benchmark for number of cells" border="0">
+     <img src="r_mfilter_benchmark_2.png" alt="benchmark for window size" border="0">
+     <br>
+     <i>Figure: Benchmark on the left shows execution time for different
+     number of cells for 9x9 matrix, benchmark on the right shows execution time
+     for 16 billion cells for different matrix sizes. (Intel Core i9-10940X CPU @ 3.30GHz x 28) </i>
+     </div>
+<p> Note that parallelization is implemented only for the parallel filter,
+not the sequential one.
+To take advantage of the parallelization, GRASS GIS
+needs to compiled with OpenMP enabled.
+
+
 <h2>NOTES</h2>
 <h2>NOTES</h2>
 
 
 If the resolution of the geographic region does not agree with the
 If the resolution of the geographic region does not agree with the
@@ -139,14 +158,16 @@ is set properly.
 <a href="g.region.html">g.region</a>,
 <a href="g.region.html">g.region</a>,
 <a href="r.clump.html">r.clump</a>,
 <a href="r.clump.html">r.clump</a>,
 <a href="r.neighbors.html">r.neighbors</a>,
 <a href="r.neighbors.html">r.neighbors</a>,
-<a href="r.resamp.filter.html">r.resamp.filter</a>
+<a href="r.resamp.filter.html">r.resamp.filter</a>,
+<a href="https://grasswiki.osgeo.org/wiki/Raster_Parallelization_with_OpenMP">Raster Parallelization with OpenMP</a>
 </em>
 </em>
 
 
 <h2>AUTHOR</h2>
 <h2>AUTHOR</h2>
 
 
 Glynn Clements.
 Glynn Clements.
 Based upon r.mfilter, by Michael Shapiro,
 Based upon r.mfilter, by Michael Shapiro,
-U.S.Army Construction Engineering Research Laboratory
+U.S.Army Construction Engineering Research Laboratory.<br>
+Aaron Saw Min Sern (OpenMP support).
 
 
 <!--
 <!--
 <p>
 <p>

BIN
raster/r.mfilter/r_mfilter_benchmark_1.png


BIN
raster/r.mfilter/r_mfilter_benchmark_2.png


+ 434 - 0
raster/r.mfilter/testsuite/test_r_mfilter.py

@@ -0,0 +1,434 @@
+from tempfile import NamedTemporaryFile
+from grass.gunittest.case import TestCase
+from grass.gunittest.main import test
+
+
+class TestNeighbors(TestCase):
+    """
+
+    Used dataset: nc_spm_full_v2alphav2
+
+    Test cases:
+    test_sequential: Test output with sequential filter type
+    test_parallel: Test output with parallel filter type
+    test_sequential_null: Test output with sequential filter type
+        with null mode enabled
+    test_parallel_null: Test output with parallel filter type
+        with null mode enabled
+    """
+
+    test_results = {
+        "test_sequential": {
+            "n": 2025000,
+            "null_cells": 0,
+            "cells": 2025000,
+            "min": 55.5787925720215,
+            "max": 156.124557495117,
+            "range": 100.545764923096,
+            "mean": 110.374947784588,
+            "mean_of_abs": 110.374947784588,
+            "stddev": 20.2637480154117,
+            "variance": 410.6194836321,
+            "coeff_var": 18.3590102846157,
+            "sum": 223509269.26379,
+        },
+        "test_parallel": {
+            "n": 2025000,
+            "null_cells": 0,
+            "cells": 2025000,
+            "min": 55.5787925720215,
+            "max": 156.223892211914,
+            "range": 100.645099639893,
+            "mean": 110.375174079437,
+            "mean_of_abs": 110.375174079437,
+            "stddev": 20.2824544942723,
+            "variance": 411.377960312227,
+            "coeff_var": 18.3759207298509,
+            "sum": 223509727.51086,
+        },
+        "test_sequential_null": {
+            False: {
+                "n": 1789490,
+                "null_cells": 235510,
+                "cells": 2025000,
+                "min": 34301.08203125,
+                "max": 43599.45703125,
+                "range": 9298.375,
+                "mean": 39040.3073035648,
+                "mean_of_abs": 39040.3073035648,
+                "stddev": 338.861109540213,
+                "variance": 114826.851558824,
+                "coeff_var": 0.867977567147046,
+                "sum": 69862239516.6562,
+            },
+            True: {
+                "n": 1789490,
+                "null_cells": 235510,
+                "cells": 2025000,
+                "min": 34300,
+                "max": 43600,
+                "range": 9300,
+                "mean": 39041.4984470043,
+                "mean_of_abs": 39041.4984470043,
+                "stddev": 348.205753496913,
+                "variance": 121247.246768353,
+                "coeff_var": 0.891886242454486,
+                "sum": 69864371055.9297,
+            },
+        },
+        "test_parallel_null": {
+            False: {
+                "n": 46381,
+                "null_cells": 1978619,
+                "cells": 2025000,
+                "min": 34300,
+                "max": 43600,
+                "range": 9300,
+                "mean": 38988.3010371973,
+                "mean_of_abs": 38988.3010371973,
+                "stddev": 792.424493234645,
+                "variance": 627936.577478183,
+                "coeff_var": 2.03246736111589,
+                "sum": 1808316390.40625,
+            },
+            True: {
+                "n": 46381,
+                "null_cells": 1978619,
+                "cells": 2025000,
+                "min": 34300,
+                "max": 43600,
+                "range": 9300,
+                "mean": 38988.2992790859,
+                "mean_of_abs": 38988.2992790859,
+                "stddev": 798.805978312437,
+                "variance": 638090.990987689,
+                "coeff_var": 2.04883514562774,
+                "sum": 1808316308.86328,
+            },
+        },
+        "test_multiple_filters": {
+            "n": 2025000,
+            "null_cells": 0,
+            "cells": 2025000,
+            "min": 55.5787925720215,
+            "max": 155.957977294922,
+            "range": 100.3791847229,
+            "mean": 110.374591878668,
+            "mean_of_abs": 110.374591878668,
+            "stddev": 20.235686035401,
+            "variance": 409.482989323322,
+            "coeff_var": 18.3336451722925,
+            "sum": 223508548.554302,
+        },
+        "test_repeated_filters": {
+            "n": 2025000,
+            "null_cells": 0,
+            "cells": 2025000,
+            "min": 55.5787925720215,
+            "max": 155.465209960938,
+            "range": 99.886417388916,
+            "mean": 110.373036950725,
+            "mean_of_abs": 110.373036950725,
+            "stddev": 20.1413729988748,
+            "variance": 405.674906279802,
+            "coeff_var": 18.2484541110042,
+            "sum": 223505399.825218,
+        },
+    }
+
+    filter_options = {
+        "sequential": b"""MATRIX 5
+                          1 1 1 1 1
+                          1 1 1 1 1
+                          1 1 1 1 1
+                          1 1 1 1 1
+                          1 1 1 1 1
+                          DIVISOR 0
+                          TYPE    S""",
+        "parallel": b"""MATRIX 5
+                        1 1 1 1 1
+                        1 1 1 1 1
+                        1 1 1 1 1
+                        1 1 1 1 1
+                        1 1 1 1 1
+                        DIVISOR 0
+                        TYPE    P""",
+        "mix": b"""MATRIX 5
+                   1 1 1 1 1
+                   1 1 1 1 1
+                   1 1 1 1 1
+                   1 1 1 1 1
+                   1 1 1 1 1
+                   DIVISOR 0
+                   TYPE    P
+
+                   MATRIX 5
+                   1 1 1 1 1
+                   1 1 1 1 1
+                   1 1 1 1 1
+                   1 1 1 1 1
+                   1 1 1 1 1
+                   DIVISOR 0
+                   TYPE    S
+
+                   MATRIX 3
+                   1 1 1 
+                   1 1 1 
+                   1 1 1 
+                   DIVISOR 9
+                   TYPE    P""",
+    }
+
+    # TODO: replace by unified handing of maps
+    to_remove = []
+
+    def create_filter(self, options):
+        """Create a temporary filter file with the given name and options."""
+        f = NamedTemporaryFile()
+        f.write(options)
+        f.flush()
+        return f
+
+    @classmethod
+    def setUpClass(cls):
+        cls.use_temp_region()
+        cls.runModule("g.region", raster="elevation")
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.del_temp_region()
+        if cls.to_remove:
+            cls.runModule(
+                "g.remove", flags="f", type="raster", name=",".join(cls.to_remove)
+            )
+
+    def test_sequential(self):
+        """Test output with sequential filter type."""
+        test_case = "test_sequential"
+        output = "{}_raster".format(test_case)
+        output_threaded = "{}_threaded_raster".format(test_case)
+        self.to_remove.extend([output, output_threaded])
+
+        filter = self.create_filter(self.filter_options["sequential"])
+        self.assertModule(
+            "r.mfilter",
+            input="elevation",
+            output=output,
+            filter=filter.name,
+        )
+        self.assertModule(
+            "r.mfilter",
+            input="elevation",
+            output=output_threaded,
+            filter=filter.name,
+            nprocs=4,
+        )
+        filter.close()
+        self.assertRasterFitsUnivar(
+            raster=output,
+            reference=self.test_results[test_case],
+            precision=1e-5,
+        )
+        self.assertRasterFitsUnivar(
+            raster=output_threaded,
+            reference=self.test_results[test_case],
+            precision=1e-5,
+        )
+
+    def test_parallel(self):
+        """Test output with parallel filter type."""
+        test_case = "test_parallel"
+        output = "{}_raster".format(test_case)
+        output_threaded = "{}_threaded_raster".format(test_case)
+        self.to_remove.extend([output, output_threaded])
+
+        filter = self.create_filter(self.filter_options["parallel"])
+        self.assertModule(
+            "r.mfilter",
+            input="elevation",
+            output=output,
+            filter=filter.name,
+        )
+        self.assertModule(
+            "r.mfilter",
+            input="elevation",
+            output=output_threaded,
+            filter=filter.name,
+            nprocs=4,
+        )
+        filter.close()
+        self.assertRasterFitsUnivar(
+            raster=output,
+            reference=self.test_results[test_case],
+            precision=1e-5,
+        )
+        self.assertRasterFitsUnivar(
+            raster=output_threaded,
+            reference=self.test_results[test_case],
+            precision=1e-5,
+        )
+
+    def test_sequential_null(self):
+        """Test output with sequential filter type with null mode enabled."""
+        test_case = "test_sequential_null"
+        output = "{}_raster".format(test_case)
+        output_z = "{}_z_raster".format(test_case)
+        self.to_remove.extend([output, output_z])
+
+        filter = self.create_filter(self.filter_options["sequential"])
+        self.assertModule(
+            "r.mfilter",
+            input="lakes",
+            output=output,
+            filter=filter.name,
+        )
+        self.assertModule(
+            "r.mfilter",
+            input="lakes",
+            output=output_z,
+            filter=filter.name,
+            flags="z",
+        )
+        filter.close()
+        self.assertRasterFitsUnivar(
+            raster=output,
+            reference=self.test_results[test_case][False],
+            precision=1e-5,
+        )
+        self.assertRasterFitsUnivar(
+            raster=output_z,
+            reference=self.test_results[test_case][True],
+            precision=1e-5,
+        )
+
+    def test_parallel_null(self):
+        """Test output with parallel filter type with null mode enabled."""
+        test_case = "test_parallel_null"
+        output = "{}_raster".format(test_case)
+        output_threaded = "{}_threaded_raster".format(test_case)
+        output_z = "{}_z_raster".format(test_case)
+        output_z_threaded = "{}_z_threaded_raster".format(test_case)
+        self.to_remove.extend([output, output_threaded, output_z, output_z_threaded])
+
+        filter = self.create_filter(self.filter_options["parallel"])
+        self.assertModule(
+            "r.mfilter",
+            input="lakes",
+            output=output,
+            filter=filter.name,
+        )
+        self.assertModule(
+            "r.mfilter",
+            input="lakes",
+            output=output_threaded,
+            filter=filter.name,
+            nprocs=4,
+        )
+        self.assertModule(
+            "r.mfilter",
+            input="lakes",
+            output=output_z,
+            filter=filter.name,
+            flags="z",
+        )
+        self.assertModule(
+            "r.mfilter",
+            input="lakes",
+            output=output_z_threaded,
+            filter=filter.name,
+            flags="z",
+            nprocs=4,
+        )
+        filter.close()
+        self.assertRasterFitsUnivar(
+            raster=output,
+            reference=self.test_results[test_case][False],
+            precision=1e-5,
+        )
+        self.assertRasterFitsUnivar(
+            raster=output_threaded,
+            reference=self.test_results[test_case][False],
+            precision=1e-5,
+        )
+        self.assertRasterFitsUnivar(
+            raster=output_z,
+            reference=self.test_results[test_case][True],
+            precision=1e-5,
+        )
+        self.assertRasterFitsUnivar(
+            raster=output_z_threaded,
+            reference=self.test_results[test_case][True],
+            precision=1e-5,
+        )
+
+    def test_multiple_filters(self):
+        """Test output with multiple filters."""
+        test_case = "test_multiple_filters"
+        output = "{}_raster".format(test_case)
+        output_threaded = "{}_threaded_raster".format(test_case)
+        self.to_remove.extend([output, output_threaded])
+
+        filter = self.create_filter(self.filter_options["mix"])
+        self.assertModule(
+            "r.mfilter",
+            input="elevation",
+            output=output,
+            filter=filter.name,
+        )
+        self.assertModule(
+            "r.mfilter",
+            input="elevation",
+            output=output_threaded,
+            filter=filter.name,
+            nprocs=4,
+        )
+        filter.close()
+        self.assertRasterFitsUnivar(
+            raster=output,
+            reference=self.test_results[test_case],
+            precision=1e-5,
+        )
+        self.assertRasterFitsUnivar(
+            raster=output_threaded,
+            reference=self.test_results[test_case],
+            precision=1e-5,
+        )
+
+    def test_repeated_filters(self):
+        """Test output with repeated filters."""
+        test_case = "test_repeated_filters"
+        output = "{}_raster".format(test_case)
+        output_threaded = "{}_threaded_raster".format(test_case)
+        self.to_remove.extend([output, output_threaded])
+
+        filter = self.create_filter(self.filter_options["mix"])
+        self.assertModule(
+            "r.mfilter",
+            input="elevation",
+            output=output,
+            filter=filter.name,
+            repeat=3,
+        )
+        self.assertModule(
+            "r.mfilter",
+            input="elevation",
+            output=output_threaded,
+            filter=filter.name,
+            repeat=3,
+            nprocs=4,
+        )
+        filter.close()
+        self.assertRasterFitsUnivar(
+            raster=output,
+            reference=self.test_results[test_case],
+            precision=1e-5,
+        )
+        self.assertRasterFitsUnivar(
+            raster=output_threaded,
+            reference=self.test_results[test_case],
+            precision=1e-5,
+        )
+
+
+if __name__ == "__main__":
+    test()