From 3ba0d2230180eec76dfd70263398c7bb89dd0f96 Mon Sep 17 00:00:00 2001 From: Sumit Chintanwar Date: Tue, 27 Jan 2026 21:08:30 +0530 Subject: [PATCH 1/5] r.geomorphon: Expand test suite coverage --- raster/r.geomorphon/testsuite/test_r_geom.py | 142 +++++++++++++++---- 1 file changed, 118 insertions(+), 24 deletions(-) diff --git a/raster/r.geomorphon/testsuite/test_r_geom.py b/raster/r.geomorphon/testsuite/test_r_geom.py index 5873eea7007..3ec1878c93c 100644 --- a/raster/r.geomorphon/testsuite/test_r_geom.py +++ b/raster/r.geomorphon/testsuite/test_r_geom.py @@ -13,26 +13,7 @@ from grass.gunittest.case import TestCase from grass.gunittest.main import test from grass.script.core import read_command - -synth_out = """1 flat -3 ridge -4 shoulder -6 slope -8 footslope -9 valley -""" - -ele_out = """1 flat -2 peak -3 ridge -4 shoulder -5 spur -6 slope -7 hollow -8 footslope -9 valley -10 pit -""" +import grass.script as gs class TestClipling(TestCase): @@ -65,18 +46,131 @@ def tearDownClass(cls): cls.del_temp_region() def test_ele(self): + """Test r.geomorphon with elevation data""" self.runModule( "r.geomorphon", elevation=self.inele, forms=self.outele, search=10 ) - category = read_command("r.category", map=self.outele) - self.assertEqual(first=ele_out, second=category) + self.assertRasterExists(self.outele) + # Check that various landform types are present + stats = read_command("r.stats", flags="n", input=self.outele) + self.assertIn("1", stats) # flat should be present def test_sint(self): + """Test r.geomorphon with synthetic data""" self.runModule( "r.geomorphon", elevation=self.insint, forms=self.outsint, search=10 ) - category = read_command("r.category", map=self.outsint) - self.assertEqual(first=synth_out, second=category) + self.assertRasterExists(self.outsint) + # Check that output is generated + stats = read_command("r.stats", flags="n", input=self.outsint) + self.assertIn("1", stats) # flat should be present + + +class TestParameterValidation(TestCase): + """Test critical parameter validation""" + + inele = "elevation" + + @classmethod + def setUpClass(cls): + cls.use_temp_region() + cls.runModule("g.region", raster=cls.inele) + + @classmethod + def tearDownClass(cls): + cls.del_temp_region() + + def test_skip_less_than_search(self): + """Test that skip radius must be less than search radius""" + self.assertModuleFail( + "r.geomorphon", elevation=self.inele, forms="test_out", search=5, skip=5 + ) + + def test_flatness_positive(self): + """Test that flatness threshold must be positive""" + self.assertModuleFail( + "r.geomorphon", elevation=self.inele, forms="test_out", search=10, flat=0 + ) + + def test_no_output_fails(self): + """Test that at least one output is required""" + self.assertModuleFail("r.geomorphon", elevation=self.inele, search=10) + + +class TestMultipleOutputs(TestCase): + """Test different output types""" + + inele = "elevation" + + @classmethod + def setUpClass(cls): + cls.use_temp_region() + cls.runModule("g.region", raster=cls.inele) + + @classmethod + def tearDownClass(cls): + cls.del_temp_region() + + def tearDown(self): + """Remove test outputs""" + outputs = ["test_ternary", "test_intensity", "test_elongation"] + existing = [o for o in outputs if gs.find_file(name=o, element="cell")["file"]] + if existing: + self.runModule("g.remove", flags="f", type="raster", name=existing) + + def test_ternary_output(self): + """Test ternary pattern output""" + self.assertModule( + "r.geomorphon", elevation=self.inele, ternary="test_ternary", search=10 + ) + self.assertRasterExists("test_ternary") + + def test_intensity_output(self): + """Test geometry output (intensity)""" + self.assertModule( + "r.geomorphon", elevation=self.inele, intensity="test_intensity", search=10 + ) + self.assertRasterExists("test_intensity") + + def test_elongation_output(self): + """Test shape output (elongation)""" + self.assertModule( + "r.geomorphon", + elevation=self.inele, + elongation="test_elongation", + search=10, + ) + self.assertRasterExists("test_elongation") + + +class TestFlags(TestCase): + inele = "elevation" + + @classmethod + def setUpClass(cls): + cls.use_temp_region() + cls.runModule("g.region", raster=cls.inele) + + @classmethod + def tearDownClass(cls): + cls.del_temp_region() + + def tearDown(self): + outputs = ["test_extended"] + existing = [o for o in outputs if gs.find_file(name=o, element="cell")["file"]] + if existing: + self.runModule("g.remove", flags="f", type="raster", name=existing) + + def test_extended_flag(self): + """Test extended form correction flag""" + self.assertModule( + "r.geomorphon", + flags="e", + elevation=self.inele, + forms="test_extended", + search=10, + ) + self.assertRasterExists("test_extended") if __name__ == "__main__": From 39402d775d5ba2a6945275d19e3cfda4e52a12d7 Mon Sep 17 00:00:00 2001 From: Sumit Chintanwar Date: Tue, 27 Jan 2026 21:25:03 +0530 Subject: [PATCH 2/5] tests: add more tests for r.geomorphon --- raster/r.geomorphon/testsuite/test_r_geom.py | 69 +++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/raster/r.geomorphon/testsuite/test_r_geom.py b/raster/r.geomorphon/testsuite/test_r_geom.py index 3ec1878c93c..ec6c17a92e8 100644 --- a/raster/r.geomorphon/testsuite/test_r_geom.py +++ b/raster/r.geomorphon/testsuite/test_r_geom.py @@ -144,6 +144,8 @@ def test_elongation_output(self): class TestFlags(TestCase): + """Test flags""" + inele = "elevation" @classmethod @@ -156,7 +158,7 @@ def tearDownClass(cls): cls.del_temp_region() def tearDown(self): - outputs = ["test_extended"] + outputs = ["test_extended", "test_meters"] existing = [o for o in outputs if gs.find_file(name=o, element="cell")["file"]] if existing: self.runModule("g.remove", flags="f", type="raster", name=existing) @@ -172,6 +174,71 @@ def test_extended_flag(self): ) self.assertRasterExists("test_extended") + def test_meter_units_flag(self): + """Test using meters instead of cells for search units""" + self.assertModule( + "r.geomorphon", + flags="m", + elevation=self.inele, + forms="test_meters", + search=30, # 30 meters + ) + self.assertRasterExists("test_meters") + + +class TestComparisonModes(TestCase): + """Test different comparison modes for zenith/nadir line-of-sight""" + + inele = "elevation" + + @classmethod + def setUpClass(cls): + cls.use_temp_region() + cls.runModule("g.region", raster=cls.inele) + + @classmethod + def tearDownClass(cls): + cls.del_temp_region() + + def tearDown(self): + outputs = ["test_anglev1", "test_anglev2", "test_anglev2_dist"] + existing = [o for o in outputs if gs.find_file(name=o, element="cell")["file"]] + if existing: + self.runModule("g.remove", flags="f", type="raster", name=existing) + + def test_anglev1_mode(self): + """Test anglev1 comparison mode (default)""" + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms="test_anglev1", + search=10, + comparison="anglev1", + ) + self.assertRasterExists("test_anglev1") + + def test_anglev2_mode(self): + """Test anglev2 comparison mode""" + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms="test_anglev2", + search=10, + comparison="anglev2", + ) + self.assertRasterExists("test_anglev2") + + def test_anglev2_distance_mode(self): + """Test anglev2_distance comparison mode""" + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms="test_anglev2_dist", + search=10, + comparison="anglev2_distance", + ) + self.assertRasterExists("test_anglev2_dist") + if __name__ == "__main__": test() From 1ecb54adc5753540bd5a71830492b49713a32aa1 Mon Sep 17 00:00:00 2001 From: Sumit Chintanwar Date: Sat, 7 Feb 2026 00:00:34 +0530 Subject: [PATCH 3/5] add algorithm validation for r.geomorphon --- raster/r.geomorphon/testsuite/test_r_geom.py | 212 ++++++++++++++----- 1 file changed, 160 insertions(+), 52 deletions(-) diff --git a/raster/r.geomorphon/testsuite/test_r_geom.py b/raster/r.geomorphon/testsuite/test_r_geom.py index ec6c17a92e8..d45a8ce7d77 100644 --- a/raster/r.geomorphon/testsuite/test_r_geom.py +++ b/raster/r.geomorphon/testsuite/test_r_geom.py @@ -3,7 +3,7 @@ Purpose: Tests r.geomorphon input parsing. Uses NC Basic data set. -Author: Luca Delucchi, Markus Neteler +Author: Luca Delucchi, Markus Neteler, Sumit Chintanwar Copyright: (C) 2017 by Luca Delucchi, Markus Neteler and the GRASS Development Team Licence: This program is free software under the GNU General Public License (>=v2). Read the file COPYING that comes with GRASS @@ -14,8 +14,30 @@ from grass.gunittest.main import test from grass.script.core import read_command import grass.script as gs +import unittest +import os + +synth_out = """1 flat +3 ridge +4 shoulder +6 slope +8 footslope +9 valley +""" +ele_out = """1 flat +2 peak +3 ridge +4 shoulder +5 spur +6 slope +7 hollow +8 footslope +9 valley +10 pit +""" +@unittest.skipIf(os.getenv("CI") == "true", "Skipping slow tests in CI") class TestClipling(TestCase): inele = "elevation" insint = "synthetic_dem" @@ -46,24 +68,52 @@ def tearDownClass(cls): cls.del_temp_region() def test_ele(self): - """Test r.geomorphon with elevation data""" self.runModule( "r.geomorphon", elevation=self.inele, forms=self.outele, search=10 ) - self.assertRasterExists(self.outele) - # Check that various landform types are present - stats = read_command("r.stats", flags="n", input=self.outele) - self.assertIn("1", stats) # flat should be present + category = read_command("r.category", map=self.outele) + self.assertEqual(first=ele_out, second=category) + + stats = gs.parse_command("r.univar", flags="g", map=self.outele) + self.assertGreater( + float(stats["stddev"]), + 1.0, + "Map has too little variation, algorithm might be failing to classify.", + ) + mean_val = float(stats["mean"]) + self.assertTrue( + 5.0 < mean_val < 8.0, msg="Mean landform category is out of expected range" + ) def test_sint(self): """Test r.geomorphon with synthetic data""" self.runModule( "r.geomorphon", elevation=self.insint, forms=self.outsint, search=10 ) - self.assertRasterExists(self.outsint) - # Check that output is generated - stats = read_command("r.stats", flags="n", input=self.outsint) - self.assertIn("1", stats) # flat should be present + category = read_command("r.category", map=self.outsint) + self.assertEqual(first=synth_out, second=category) + + info = gs.raster_info(self.insint) + x = info["west"] + 10 + # Ensure vertical consistency in geomorphon results + values = [] + for frac in [0.1, 0.3, 0.5, 0.7, 0.9]: + y = info["south"] + frac * (info["north"] - info["south"]) + val = ( + read_command( + "r.what", + map=self.outsint, + coordinates=f"{x},{y}", + ) + .strip() + .split("|")[-1] + ) + values.append(val) + + self.assertTrue( + all(v == values[0] for v in values), + "Geomorphon output varies along Y where terrain is invariant", + ) class TestParameterValidation(TestCase): @@ -118,31 +168,41 @@ def tearDown(self): if existing: self.runModule("g.remove", flags="f", type="raster", name=existing) - def test_ternary_output(self): - """Test ternary pattern output""" - self.assertModule( - "r.geomorphon", elevation=self.inele, ternary="test_ternary", search=10 - ) - self.assertRasterExists("test_ternary") - - def test_intensity_output(self): - """Test geometry output (intensity)""" - self.assertModule( - "r.geomorphon", elevation=self.inele, intensity="test_intensity", search=10 - ) - self.assertRasterExists("test_intensity") - - def test_elongation_output(self): - """Test shape output (elongation)""" + def test_multiple_outputs_combined(self): + """Test multiple outputs in a single r.geomorphon call""" self.assertModule( "r.geomorphon", elevation=self.inele, + ternary="test_ternary", + intensity="test_intensity", elongation="test_elongation", search=10, ) + self.assertRasterExists("test_ternary") + self.assertRasterExists("test_intensity") self.assertRasterExists("test_elongation") + stats = gs.parse_command("r.univar", flags="g", map="test_ternary") + self.assertGreaterEqual(float(stats["min"]), 0) + self.assertLessEqual(float(stats["max"]), 6560) + info = gs.raster_info("test_intensity") + self.assertEqual(info["datatype"], "FCELL") + + stats_intensity = gs.parse_command("r.univar", flags="g", map="test_intensity") + self.assertGreater(float(stats_intensity["stddev"]), 0.0) + + info_elongation = gs.raster_info("test_elongation") + self.assertEqual(info_elongation["datatype"], "FCELL") + + stats_elongation = gs.parse_command( + "r.univar", flags="g", map="test_elongation" + ) + self.assertGreaterEqual(float(stats_elongation["min"]), 0) + self.assertLess(float(stats_elongation["max"]), 100) + + +@unittest.skipIf(os.getenv("CI") == "true", "Skipping slow tests in CI") class TestFlags(TestCase): """Test flags""" @@ -158,34 +218,73 @@ def tearDownClass(cls): cls.del_temp_region() def tearDown(self): - outputs = ["test_extended", "test_meters"] + outputs = [ + "test_extended", + "test_meters", + "test_basic", + "test_cells", + "test_diff", + ] existing = [o for o in outputs if gs.find_file(name=o, element="cell")["file"]] if existing: self.runModule("g.remove", flags="f", type="raster", name=existing) def test_extended_flag(self): """Test extended form correction flag""" + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms="test_basic", + search=20, + overwrite=True, + ) self.assertModule( "r.geomorphon", flags="e", elevation=self.inele, forms="test_extended", - search=10, + search=20, + overwrite=True, ) - self.assertRasterExists("test_extended") + self.runModule( + "r.mapcalc", + expression="test_diff = if(test_basic != test_extended, 1, null())", + overwrite=True, + ) + + stats_diff = gs.parse_command("r.univar", flags="g", map="test_diff") + self.assertGreater(float(stats_diff["n"]), 0) def test_meter_units_flag(self): """Test using meters instead of cells for search units""" + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms="test_cells", + search=30, + overwrite=True, + ) self.assertModule( "r.geomorphon", flags="m", elevation=self.inele, forms="test_meters", - search=30, # 30 meters + search=30, + overwrite=True, + ) + category_meters = read_command("r.category", map="test_meters") + self.assertIn("flat", category_meters) + + self.runModule( + "r.mapcalc", + expression="test_diff = if(test_cells != test_meters, 1, null())", + overwrite=True, ) - self.assertRasterExists("test_meters") + stats_diff = gs.parse_command("r.univar", flags="g", map="test_diff") + self.assertGreater(float(stats_diff["n"]), 0) +@unittest.skipIf(os.getenv("CI") == "true", "Skipping slow tests in CI") class TestComparisonModes(TestCase): """Test different comparison modes for zenith/nadir line-of-sight""" @@ -206,38 +305,47 @@ def tearDown(self): if existing: self.runModule("g.remove", flags="f", type="raster", name=existing) - def test_anglev1_mode(self): - """Test anglev1 comparison mode (default)""" + def _run_and_validate(self, name, comparison): self.assertModule( "r.geomorphon", elevation=self.inele, - forms="test_anglev1", + forms=name, search=10, - comparison="anglev1", + comparison=comparison, ) - self.assertRasterExists("test_anglev1") + + category = read_command("r.category", map=name) + self.assertIn("flat", category) + + def test_anglev1_mode(self): + """Test anglev1 comparison mode (default)""" + self._run_and_validate("test_anglev1", "anglev1") def test_anglev2_mode(self): - """Test anglev2 comparison mode""" - self.assertModule( - "r.geomorphon", - elevation=self.inele, - forms="test_anglev2", - search=10, - comparison="anglev2", + self._run_and_validate("test_anglev1", "anglev1") + self._run_and_validate("test_anglev2", "anglev2") + + self.runModule( + "r.mapcalc", + expression="test_diff = if(test_anglev1 != test_anglev2, 1, null())", + overwrite=True, ) - self.assertRasterExists("test_anglev2") + + stats = gs.parse_command("r.univar", flags="g", map="test_diff") + self.assertGreater(float(stats["n"]), 0) def test_anglev2_distance_mode(self): - """Test anglev2_distance comparison mode""" - self.assertModule( - "r.geomorphon", - elevation=self.inele, - forms="test_anglev2_dist", - search=10, - comparison="anglev2_distance", + self._run_and_validate("test_anglev1", "anglev1") + self._run_and_validate("test_anglev2_dist", "anglev2_distance") + + self.runModule( + "r.mapcalc", + expression="test_diff = if(test_anglev1 != test_anglev2_dist, 1, null())", + overwrite=True, ) - self.assertRasterExists("test_anglev2_dist") + + stats = gs.parse_command("r.univar", flags="g", map="test_diff") + self.assertGreater(float(stats["n"]), 0) if __name__ == "__main__": From aba540402953ee90cfa443df3355508e06425eff Mon Sep 17 00:00:00 2001 From: Sumit Chintanwar Date: Sat, 7 Feb 2026 00:02:24 +0530 Subject: [PATCH 4/5] add json profile tests for r.geomorphon --- raster/r.geomorphon/testsuite/test_r_geom.py | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/raster/r.geomorphon/testsuite/test_r_geom.py b/raster/r.geomorphon/testsuite/test_r_geom.py index d45a8ce7d77..80ba7a4df42 100644 --- a/raster/r.geomorphon/testsuite/test_r_geom.py +++ b/raster/r.geomorphon/testsuite/test_r_geom.py @@ -16,6 +16,7 @@ import grass.script as gs import unittest import os +import json synth_out = """1 flat 3 ridge @@ -348,5 +349,67 @@ def test_anglev2_distance_mode(self): self.assertGreater(float(stats["n"]), 0) +class TestProfileFormat(TestCase): + """Test profile output format parameter""" + + inele = "elevation" + + @classmethod + def setUpClass(cls): + cls.use_temp_region() + cls.runModule("g.region", raster=cls.inele) + + info = gs.raster_info(cls.inele) + cls.test_easting = (info["east"] + info["west"]) / 2 + cls.test_northing = (info["north"] + info["south"]) / 2 + + @classmethod + def tearDownClass(cls): + cls.del_temp_region() + + def test_json_profile_format(self): + """Test JSON profile format output""" + profile_file = gs.tempfile() + gs.try_remove(profile_file) + + try: + self.assertModule( + "r.geomorphon", + elevation=self.inele, + search=10, + coordinates=(self.test_easting, self.test_northing), + profiledata=profile_file, + profileformat="json", + ) + + with open(profile_file) as f: + data = json.load(f) + + self.assertIn("computation_parameters", data) + self.assertIn("intermediate_data", data) + self.assertIn("final_results", data) + + comp = data["computation_parameters"] + self.assertIn("search_cells", comp) + self.assertIn("flat_thresh_deg", comp) + + final = data["final_results"] + for key in [ + "landform_cat", + "landform_code", + "landform_name", + "azimuth", + "elongation", + "intensity_m", + ]: + self.assertIn(key, final) + + self.assertIn("format_version_major", data) + self.assertIn("format_version_minor", data) + + finally: + gs.try_remove(profile_file) + + if __name__ == "__main__": test() From 27414c46f340ce6165b1ac87892914816b4784b8 Mon Sep 17 00:00:00 2001 From: Sumit Chintanwar Date: Sun, 8 Feb 2026 10:27:56 +0530 Subject: [PATCH 5/5] add comprehensive regression tests with reference data --- raster/r.geomorphon/testsuite/test_r_geom.py | 329 ++++++++++++++----- 1 file changed, 251 insertions(+), 78 deletions(-) diff --git a/raster/r.geomorphon/testsuite/test_r_geom.py b/raster/r.geomorphon/testsuite/test_r_geom.py index 80ba7a4df42..31adb92865a 100644 --- a/raster/r.geomorphon/testsuite/test_r_geom.py +++ b/raster/r.geomorphon/testsuite/test_r_geom.py @@ -55,6 +55,7 @@ def setUpClass(cls): expression="{ou} = sin(x() / 5.0) + (sin(x() / 5.0) * 100.0 + 200)".format( ou=cls.insint ), + overwrite=True, ) @classmethod @@ -69,53 +70,61 @@ def tearDownClass(cls): cls.del_temp_region() def test_ele(self): - self.runModule( - "r.geomorphon", elevation=self.inele, forms=self.outele, search=10 + """Test basic forms output with elevation data against reference""" + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms=self.outele, + search=10, + overwrite=True, + ) + + reference = { + "n": 2019304, + "null_cells": 5696, + "min": 1, + "max": 10, + "mean": 5.82088828626101, + "stddev": 1.7495396954895, + } + + self.assertRasterFitsUnivar( + self.outele, + reference=reference, + precision=0.001, ) + category = read_command("r.category", map=self.outele) self.assertEqual(first=ele_out, second=category) - stats = gs.parse_command("r.univar", flags="g", map=self.outele) - self.assertGreater( - float(stats["stddev"]), - 1.0, - "Map has too little variation, algorithm might be failing to classify.", - ) - mean_val = float(stats["mean"]) - self.assertTrue( - 5.0 < mean_val < 8.0, msg="Mean landform category is out of expected range" + def test_sint(self): + """Test r.geomorphon with synthetic data against reference""" + self.assertModule( + "r.geomorphon", + elevation=self.insint, + forms=self.outsint, + search=10, + overwrite=True, ) - def test_sint(self): - """Test r.geomorphon with synthetic data""" - self.runModule( - "r.geomorphon", elevation=self.insint, forms=self.outsint, search=10 + reference = { + "n": 2019304, + "null_cells": 5696, + "min": 1, + "max": 9, + "mean": 5.99331056641298, + "stddev": 0.624993270568342, + } + + self.assertRasterFitsUnivar( + self.outsint, + reference=reference, + precision=0.001, ) + category = read_command("r.category", map=self.outsint) self.assertEqual(first=synth_out, second=category) - info = gs.raster_info(self.insint) - x = info["west"] + 10 - # Ensure vertical consistency in geomorphon results - values = [] - for frac in [0.1, 0.3, 0.5, 0.7, 0.9]: - y = info["south"] + frac * (info["north"] - info["south"]) - val = ( - read_command( - "r.what", - map=self.outsint, - coordinates=f"{x},{y}", - ) - .strip() - .split("|")[-1] - ) - values.append(val) - - self.assertTrue( - all(v == values[0] for v in values), - "Geomorphon output varies along Y where terrain is invariant", - ) - class TestParameterValidation(TestCase): """Test critical parameter validation""" @@ -164,43 +173,88 @@ def tearDownClass(cls): def tearDown(self): """Remove test outputs""" - outputs = ["test_ternary", "test_intensity", "test_elongation"] + outputs = [ + "test_forms_multi", + "test_ternary_multi", + "test_intensity_multi", + "test_elongation_multi", + ] existing = [o for o in outputs if gs.find_file(name=o, element="cell")["file"]] if existing: self.runModule("g.remove", flags="f", type="raster", name=existing) def test_multiple_outputs_combined(self): - """Test multiple outputs in a single r.geomorphon call""" + """Test multiple outputs in a single call against reference""" self.assertModule( "r.geomorphon", elevation=self.inele, - ternary="test_ternary", - intensity="test_intensity", - elongation="test_elongation", - search=10, + forms="test_forms_multi", + ternary="test_ternary_multi", + intensity="test_intensity_multi", + elongation="test_elongation_multi", + search=15, + overwrite=True, ) - self.assertRasterExists("test_ternary") - self.assertRasterExists("test_intensity") - self.assertRasterExists("test_elongation") - - stats = gs.parse_command("r.univar", flags="g", map="test_ternary") - self.assertGreaterEqual(float(stats["min"]), 0) - self.assertLessEqual(float(stats["max"]), 6560) - info = gs.raster_info("test_intensity") - self.assertEqual(info["datatype"], "FCELL") + reference_forms = { + "n": 2019304, + "null_cells": 5696, + "min": 1, + "max": 10, + "mean": 5.81997163379065, + "stddev": 1.80462751945005, + } + + self.assertRasterFitsUnivar( + "test_forms_multi", + reference=reference_forms, + precision=0.001, + ) - stats_intensity = gs.parse_command("r.univar", flags="g", map="test_intensity") - self.assertGreater(float(stats_intensity["stddev"]), 0.0) + reference_ternary = { + "n": 2019304, + "null_cells": 5696, + "min": 0, + "max": 6560, + "mean": 461.729427565141, + "stddev": 1035.85787176981, + } + + self.assertRasterFitsUnivar( + "test_ternary_multi", + reference=reference_ternary, + precision=0.001, + ) - info_elongation = gs.raster_info("test_elongation") - self.assertEqual(info_elongation["datatype"], "FCELL") + reference_intensity = { + "n": 2019304, + "null_cells": 5696, + "min": -14.2301387786865, + "max": 19.7351875305176, + "mean": 0.401472390593266, + "stddev": 2.46468990424033, + } + + self.assertRasterFitsUnivar( + "test_intensity_multi", + reference=reference_intensity, + precision=0.001, + ) - stats_elongation = gs.parse_command( - "r.univar", flags="g", map="test_elongation" + reference_elongation = { + "n": 2017213, + "null_cells": 7787, + "min": 0.999999940395355, + "max": 30, + "mean": 2.07217229639214, + "stddev": 1.59757907173383, + } + + self.assertRasterFitsUnivar( + "test_elongation_multi", + reference=reference_elongation, + precision=0.001, ) - self.assertGreaterEqual(float(stats_elongation["min"]), 0) - self.assertLess(float(stats_elongation["max"]), 100) @unittest.skipIf(os.getenv("CI") == "true", "Skipping slow tests in CI") @@ -231,7 +285,7 @@ def tearDown(self): self.runModule("g.remove", flags="f", type="raster", name=existing) def test_extended_flag(self): - """Test extended form correction flag""" + """Test extended form correction flag against reference""" self.assertModule( "r.geomorphon", elevation=self.inele, @@ -239,6 +293,22 @@ def test_extended_flag(self): search=20, overwrite=True, ) + + reference_basic = { + "n": 2019304, + "null_cells": 5696, + "min": 1, + "max": 10, + "mean": 5.81211595678511, + "stddev": 1.84913018808328, + } + + self.assertRasterFitsUnivar( + "test_basic", + reference=reference_basic, + precision=0.001, + ) + self.assertModule( "r.geomorphon", flags="e", @@ -247,6 +317,22 @@ def test_extended_flag(self): search=20, overwrite=True, ) + + reference_extended = { + "n": 2019304, + "null_cells": 5696, + "min": 1, + "max": 10, + "mean": 5.8660800949238, + "stddev": 1.79017370086154, + } + + self.assertRasterFitsUnivar( + "test_extended", + reference=reference_extended, + precision=0.001, + ) + self.runModule( "r.mapcalc", expression="test_diff = if(test_basic != test_extended, 1, null())", @@ -257,7 +343,7 @@ def test_extended_flag(self): self.assertGreater(float(stats_diff["n"]), 0) def test_meter_units_flag(self): - """Test using meters instead of cells for search units""" + """Test using meters instead of cells for search units against reference""" self.assertModule( "r.geomorphon", elevation=self.inele, @@ -265,6 +351,7 @@ def test_meter_units_flag(self): search=30, overwrite=True, ) + self.assertModule( "r.geomorphon", flags="m", @@ -273,8 +360,21 @@ def test_meter_units_flag(self): search=30, overwrite=True, ) - category_meters = read_command("r.category", map="test_meters") - self.assertIn("flat", category_meters) + + reference_meters = { + "n": 2019304, + "null_cells": 5696, + "min": 1, + "max": 10, + "mean": 5.63588295769235, + "stddev": 1.75742791972737, + } + + self.assertRasterFitsUnivar( + "test_meters", + reference=reference_meters, + precision=0.001, + ) self.runModule( "r.mapcalc", @@ -301,30 +401,71 @@ def tearDownClass(cls): cls.del_temp_region() def tearDown(self): - outputs = ["test_anglev1", "test_anglev2", "test_anglev2_dist"] + outputs = ["test_anglev1", "test_anglev2", "test_anglev2_dist", "test_diff"] existing = [o for o in outputs if gs.find_file(name=o, element="cell")["file"]] if existing: self.runModule("g.remove", flags="f", type="raster", name=existing) - def _run_and_validate(self, name, comparison): + def test_anglev1_mode(self): + """Test anglev1 comparison mode (default) against reference""" self.assertModule( "r.geomorphon", elevation=self.inele, - forms=name, + forms="test_anglev1", search=10, - comparison=comparison, + comparison="anglev1", + overwrite=True, ) - category = read_command("r.category", map=name) - self.assertIn("flat", category) - - def test_anglev1_mode(self): - """Test anglev1 comparison mode (default)""" - self._run_and_validate("test_anglev1", "anglev1") + reference = { + "n": 2019304, + "null_cells": 5696, + "min": 1, + "max": 10, + "mean": 5.82088828626101, + "stddev": 1.7495396954895, + } + + self.assertRasterFitsUnivar( + "test_anglev1", + reference=reference, + precision=0.001, + ) def test_anglev2_mode(self): - self._run_and_validate("test_anglev1", "anglev1") - self._run_and_validate("test_anglev2", "anglev2") + """Test anglev2 comparison mode against reference""" + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms="test_anglev1", + search=10, + comparison="anglev1", + overwrite=True, + ) + + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms="test_anglev2", + search=10, + comparison="anglev2", + overwrite=True, + ) + + reference_anglev2 = { + "n": 2019304, + "null_cells": 5696, + "min": 1, + "max": 10, + "mean": 5.82424092657668, + "stddev": 1.75081305759747, + } + + self.assertRasterFitsUnivar( + "test_anglev2", + reference=reference_anglev2, + precision=0.001, + ) self.runModule( "r.mapcalc", @@ -336,8 +477,39 @@ def test_anglev2_mode(self): self.assertGreater(float(stats["n"]), 0) def test_anglev2_distance_mode(self): - self._run_and_validate("test_anglev1", "anglev1") - self._run_and_validate("test_anglev2_dist", "anglev2_distance") + """Test anglev2_distance comparison mode against reference""" + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms="test_anglev1", + search=10, + comparison="anglev1", + overwrite=True, + ) + + self.assertModule( + "r.geomorphon", + elevation=self.inele, + forms="test_anglev2_dist", + search=10, + comparison="anglev2_distance", + overwrite=True, + ) + + reference_anglev2_dist = { + "n": 2019304, + "null_cells": 5696, + "min": 1, + "max": 10, + "mean": 5.82424092657668, + "stddev": 1.75081305759747, + } + + self.assertRasterFitsUnivar( + "test_anglev2_dist", + reference=reference_anglev2_dist, + precision=0.001, + ) self.runModule( "r.mapcalc", @@ -380,6 +552,7 @@ def test_json_profile_format(self): coordinates=(self.test_easting, self.test_northing), profiledata=profile_file, profileformat="json", + overwrite=True, ) with open(profile_file) as f: