From 50af5665b0fd844dd9e40a543a68a2379a7ce7b2 Mon Sep 17 00:00:00 2001 From: Sumit Chintanwar Date: Mon, 2 Feb 2026 17:33:52 +0530 Subject: [PATCH 1/4] r.watershed: add regression tests for r-watershed --- .../r.watershed/testsuite/r_watershed_test.py | 153 ++++++++++++++++-- 1 file changed, 143 insertions(+), 10 deletions(-) diff --git a/raster/r.watershed/testsuite/r_watershed_test.py b/raster/r.watershed/testsuite/r_watershed_test.py index da5939aa022..d139ce83c37 100644 --- a/raster/r.watershed/testsuite/r_watershed_test.py +++ b/raster/r.watershed/testsuite/r_watershed_test.py @@ -161,16 +161,16 @@ def test_thresholdsize(self): basin=self.basin, overwrite=True, ) - # it is expected that 100k Threshold has a min=2 and max=12 for this - # data - reference = "min=2\nmax=12" + # it is expected that 100k Threshold has a min=2 and max=20 for this + # data (north Carolina dataset, (nc_spm_08_grass7)) + reference = "min=2\nmax=20" self.assertRasterFitsUnivar( self.basin, reference=reference, - msg="Basin values must be in the range [2, 12]", + msg="Basin values must be in the range [2, 20]", ) - # it is expected that 100k Threshold has a min=2 and max=256 for this - # data + # it is expected that 100k Threshold has a min=2 and max=274 for this + # data (North carolina dataset,(nc_spm_08_grass7)) self.assertModule( "r.watershed", elevation=self.elevation, @@ -178,11 +178,11 @@ def test_thresholdsize(self): basin=self.basin, overwrite=True, ) - reference = "min=2\nmax=256" + reference = "min=2\nmax=274" self.assertRasterFitsUnivar( self.basin, reference=reference, - msg="Basin values must be in the range [2, 256]", + msg="Basin values must be in the range [2, 274]", ) def test_drainageDirection(self): @@ -208,11 +208,144 @@ def test_basinValue(self): # TODO: test just min, max is theoretically unlimited # or set a lower value according to what is expected with this data # TODO: add test which tests that 'max basin id' == 'num of basins' - reference = "min=2\nmax=256" + reference = "min=2\nmax=274" self.assertRasterFitsUnivar( self.basin, reference=reference, - msg="Basin values must be in the range [2, 256]", + msg="Basin values must be in the range [2, 274]", + ) + + def test_accumulationValues(self): + """Test if accumulation values follow expected patterns""" + self.assertModule( + "r.watershed", + elevation=self.elevation, + threshold="10000", + accumulation=self.accumulation, + ) + # Just verify the output exists and has valid statistics + self.assertRasterExists( + self.accumulation, msg="Accumulation output was not created" + ) + + def test_streamValuesConsistency(self): + """Test if stream output is created with valid values""" + self.assertModule( + "r.watershed", + elevation=self.elevation, + threshold="10000", + stream=self.stream, + ) + # Stream values should be 0 (no stream) or positive integers + self.assertRasterMinMax( + self.stream, 0, 10000, msg="Stream values out of expected range" + ) + + def test_slopeSteepnessRange(self): + """Test if slope steepness values are created""" + self.assertModule( + "r.watershed", + elevation=self.elevation, + threshold="10000", + slope_steepness=self.slopesteepness, + ) + # Verify output exists with reasonable range + self.assertRasterMinMax( + self.slopesteepness, + 0, + 100, + msg="Slope steepness out of expected range", + ) + + def test_convergenceFlag(self): + """Test the convergence parameter""" + self.assertModule( + "r.watershed", + elevation=self.elevation, + threshold="10000", + convergence="5", + accumulation=self.accumulation, + ) + self.assertRasterExists( + self.accumulation, msg="Accumulation with convergence parameter not created" + ) + + def test_memoryParameter(self): + """Test if memory parameter is accepted""" + self.assertModule( + "r.watershed", + elevation=self.elevation, + threshold="10000", + memory="300", + accumulation=self.accumulation, + ) + self.assertRasterExists( + self.accumulation, msg="Accumulation with memory parameter not created" + ) + + def test_aFlag(self): + """Test the -a flag for positive flow accumulation""" + self.assertModule( + "r.watershed", + flags="a", + elevation=self.elevation, + threshold="10000", + accumulation=self.accumulation, + ) + # Verify output is created with -a flag + self.assertRasterExists( + self.accumulation, msg="Accumulation with -a flag not created" + ) + + def test_consistentResults(self): + """Test that running twice with same parameters gives identical results""" + # First run + self.assertModule( + "r.watershed", + elevation=self.elevation, + threshold="10000", + basin=self.basin, + ) + # Store first result + self.runModule("g.copy", raster=(self.basin, "basin_copy")) + + # Second run with overwrite + self.assertModule( + "r.watershed", + elevation=self.elevation, + threshold="10000", + basin=self.basin, + overwrite=True, + ) + # Results should be identical + self.assertRastersNoDifference( + self.basin, "basin_copy", 0, msg="Results are not reproducible" + ) + self.runModule("g.remove", flags="f", type="raster", name="basin_copy") + + def test_minimumThreshold(self): + """Test that minimum valid threshold (1) works""" + self.assertModule( + "r.watershed", + elevation=self.elevation, + threshold="1", + stream=self.stream, + ) + self.assertRasterExists( + self.stream, msg="Stream output with threshold=1 not created" + ) + + def test_bFlag(self): + """Test the -b flag for beautification""" + self.assertModule( + "r.watershed", + flags="b", + elevation=self.elevation, + threshold="10000", + accumulation=self.accumulation, + ) + self.assertRasterExists( + self.accumulation, msg="Accumulation with -b flag not created" ) From 5dff710f0550236d67c0f5da83275713f56de875 Mon Sep 17 00:00:00 2001 From: Sumit Chintanwar Date: Mon, 2 Feb 2026 22:14:32 +0530 Subject: [PATCH 2/4] changed to original maximum values --- .../r.watershed/testsuite/r_watershed_test.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/raster/r.watershed/testsuite/r_watershed_test.py b/raster/r.watershed/testsuite/r_watershed_test.py index d139ce83c37..38215834054 100644 --- a/raster/r.watershed/testsuite/r_watershed_test.py +++ b/raster/r.watershed/testsuite/r_watershed_test.py @@ -161,16 +161,16 @@ def test_thresholdsize(self): basin=self.basin, overwrite=True, ) - # it is expected that 100k Threshold has a min=2 and max=20 for this - # data (north Carolina dataset, (nc_spm_08_grass7)) - reference = "min=2\nmax=20" + # it is expected that 100k Threshold has a min=2 and max=12 for this + # data + reference = "min=2\nmax=12" self.assertRasterFitsUnivar( self.basin, reference=reference, - msg="Basin values must be in the range [2, 20]", + msg="Basin values must be in the range [2, 12]", ) - # it is expected that 100k Threshold has a min=2 and max=274 for this - # data (North carolina dataset,(nc_spm_08_grass7)) + # it is expected that 100k Threshold has a min=2 and max=256 for this + # data self.assertModule( "r.watershed", elevation=self.elevation, @@ -178,11 +178,11 @@ def test_thresholdsize(self): basin=self.basin, overwrite=True, ) - reference = "min=2\nmax=274" + reference = "min=2\nmax=256" self.assertRasterFitsUnivar( self.basin, reference=reference, - msg="Basin values must be in the range [2, 274]", + msg="Basin values must be in the range [2, 256]", ) def test_drainageDirection(self): @@ -208,11 +208,11 @@ def test_basinValue(self): # TODO: test just min, max is theoretically unlimited # or set a lower value according to what is expected with this data # TODO: add test which tests that 'max basin id' == 'num of basins' - reference = "min=2\nmax=274" + reference = "min=2\nmax=256" self.assertRasterFitsUnivar( self.basin, reference=reference, - msg="Basin values must be in the range [2, 274]", + msg="Basin values must be in the range [2, 256]", ) def test_accumulationValues(self): From 6a0835743c55255524593e0e8b0da281cc7bc2dd Mon Sep 17 00:00:00 2001 From: Sumit Chintanwar Date: Sun, 8 Feb 2026 04:20:05 +0530 Subject: [PATCH 3/4] expand tests with better regression coverage --- .../r.watershed/testsuite/r_watershed_test.py | 297 +++++++++++------- 1 file changed, 190 insertions(+), 107 deletions(-) diff --git a/raster/r.watershed/testsuite/r_watershed_test.py b/raster/r.watershed/testsuite/r_watershed_test.py index 38215834054..05c37c5c83f 100644 --- a/raster/r.watershed/testsuite/r_watershed_test.py +++ b/raster/r.watershed/testsuite/r_watershed_test.py @@ -28,6 +28,8 @@ class TestWatershed(TestCase): elevation = "elevation" lengthslope_2 = "test_lengthslope_2" stream_2 = "test_stream_2" + tci = "test_tci" + spi = "test_spi" @classmethod def setUpClass(cls): @@ -50,17 +52,7 @@ def tearDown(self): "g.remove", flags="f", type="raster", - name=[ - self.accumulation, - self.drainage, - self.basin, - self.stream, - self.halfbasin, - self.slopelength, - self.slopesteepness, - self.lengthslope_2, - self.stream_2, - ], + pattern="test_*", ) def test_OutputCreated(self): @@ -149,167 +141,273 @@ def test_watershedThreadholdfail(self): threshold="-1", stream=self.stream, overwrite=True, - msg="Threshold value of 0 considered valid.", + msg="Threshold value of -1 considered valid.", ) - def test_thresholdsize(self): - """Test the expected range of basin output values""" + def test_drainageDirection(self): + """Test if the drainage direction is between -8 and 8.""" self.assertModule( "r.watershed", elevation=self.elevation, threshold="100000", - basin=self.basin, - overwrite=True, + drainage=self.drainage, + ) + # Make sure the min/max is between -8 and 8 + self.assertRasterMinMax( + self.drainage, -8, 8, msg="Direction must be between -8 and 8" + ) + + def test_accumulation_mfd(self): + """Test MFD flow accumulation against reference statistics.""" + self.assertModule( + "r.watershed", + elevation=self.elevation, + threshold="10000", + accumulation=self.accumulation, ) - # it is expected that 100k Threshold has a min=2 and max=12 for this - # data - reference = "min=2\nmax=12" + + reference = { + "n": 2602530, + "null_cells": 16180, + "min": -832420.692197234, + "max": 429067.069562766, + "mean": -343.747566806773, + "stddev": 16259.307703876, + } + self.assertRasterFitsUnivar( - self.basin, + self.accumulation, + reference=reference, + precision=0.001, + ) + + def test_accumulation_sfd(self): + """Test SFD flow accumulation against reference statistics.""" + self.assertModule( + "r.watershed", + flags="s", + elevation=self.elevation, + threshold="10000", + accumulation=self.accumulation, + ) + + reference = { + "n": 2602530, + "null_cells": 16180, + "min": -832531, + "max": 441312, + "mean": -287.797973894633, + "stddev": 16793.5124227608, + } + + self.assertRasterFitsUnivar( + self.accumulation, reference=reference, - msg="Basin values must be in the range [2, 12]", + precision=0.001, ) - # it is expected that 100k Threshold has a min=2 and max=256 for this - # data + + def test_basin_threshold_10k(self): + """Test basin delineation with threshold=10000.""" self.assertModule( "r.watershed", elevation=self.elevation, threshold="10000", basin=self.basin, - overwrite=True, ) - reference = "min=2\nmax=256" + + reference = { + "n": 2456668, + "null_cells": 162042, + "min": 2, + "max": 274, + "mean": 142.826128723946, + "stddev": 86.332946971229, + } + self.assertRasterFitsUnivar( self.basin, reference=reference, - msg="Basin values must be in the range [2, 256]", + precision=0.001, ) - def test_drainageDirection(self): - """Test if the drainage direction is between -8 and 8.""" + def test_basin_threshold_100k(self): + """Test basin delineation with threshold=100000.""" self.assertModule( "r.watershed", elevation=self.elevation, threshold="100000", - drainage=self.drainage, - ) - # Make sure the min/max is between -8 and 8 - self.assertRasterMinMax( - self.drainage, -8, 8, msg="Direction must be between -8 and 8" + basin=self.basin, ) - def test_basinValue(self): - """Check to see if the basin value is 0 or greater""" - self.assertModule( - "r.watershed", elevation=self.elevation, threshold="10000", basin=self.basin - ) - # Make sure the minimum value is 0 for basin value representing unique - # positive integer. - # TODO: test just min, max is theoretically unlimited - # or set a lower value according to what is expected with this data - # TODO: add test which tests that 'max basin id' == 'num of basins' - reference = "min=2\nmax=256" + reference = { + "n": 2026515, + "null_cells": 592195, + "min": 2, + "max": 20, + "mean": 12.6790475274054, + "stddev": 5.81111419543262, + } + self.assertRasterFitsUnivar( self.basin, reference=reference, - msg="Basin values must be in the range [2, 256]", + precision=0.001, ) - def test_accumulationValues(self): - """Test if accumulation values follow expected patterns""" + def test_stream_network(self): + """Test stream network delineation.""" self.assertModule( "r.watershed", elevation=self.elevation, threshold="10000", - accumulation=self.accumulation, + stream=self.stream, ) - # Just verify the output exists and has valid statistics - self.assertRasterExists( - self.accumulation, msg="Accumulation output was not created" + + reference = { + "n": 15740, + "null_cells": 2602970, + "min": 2, + "max": 274, + "mean": 141.941041931385, + "stddev": 83.9583249945486, + } + + self.assertRasterFitsUnivar( + self.stream, + reference=reference, + precision=0.001, ) - def test_streamValuesConsistency(self): - """Test if stream output is created with valid values""" + def test_half_basin(self): + """Test half basin delineation.""" self.assertModule( "r.watershed", elevation=self.elevation, threshold="10000", - stream=self.stream, + half_basin=self.halfbasin, ) - # Stream values should be 0 (no stream) or positive integers - self.assertRasterMinMax( - self.stream, 0, 10000, msg="Stream values out of expected range" + + reference = { + "n": 2456668, + "null_cells": 162042, + "min": 1, + "max": 274, + "mean": 142.308489384809, + "stddev": 86.3166284098813, + } + + self.assertRasterFitsUnivar( + self.halfbasin, + reference=reference, + precision=0.001, ) - def test_slopeSteepnessRange(self): - """Test if slope steepness values are created""" + def test_slope_steepness(self): + """Test slope steepness calculation.""" self.assertModule( "r.watershed", elevation=self.elevation, threshold="10000", slope_steepness=self.slopesteepness, ) - # Verify output exists with reasonable range - self.assertRasterMinMax( + + reference = { + "n": 2602530, + "null_cells": 16180, + "min": 0.03, + "max": 3.07919076172568, + "mean": 0.153479116301884, + "stddev": 0.163185729045392, + } + + self.assertRasterFitsUnivar( self.slopesteepness, - 0, - 100, - msg="Slope steepness out of expected range", + reference=reference, + precision=0.001, ) - def test_convergenceFlag(self): - """Test the convergence parameter""" + def test_length_slope(self): + """Test Length Slope calculation.""" self.assertModule( "r.watershed", elevation=self.elevation, threshold="10000", - convergence="5", - accumulation=self.accumulation, + length_slope=self.slopelength, ) - self.assertRasterExists( - self.accumulation, msg="Accumulation with convergence parameter not created" + + reference = { + "n": 2602530, + "null_cells": 16180, + "min": 0.03, + "max": 5.98881244191164, + "mean": 0.192025694191372, + "stddev": 0.240997329983397, + } + + self.assertRasterFitsUnivar( + self.slopelength, + reference=reference, + precision=0.001, ) - def test_memoryParameter(self): - """Test if memory parameter is accepted""" + def test_tci(self): + """Test TCI calculation.""" self.assertModule( "r.watershed", elevation=self.elevation, threshold="10000", - memory="300", - accumulation=self.accumulation, + tci=self.tci, ) - self.assertRasterExists( - self.accumulation, msg="Accumulation with memory parameter not created" + + reference = { + "n": 2596072, + "null_cells": 22638, + "min": 1.94904979310483, + "max": 26.8104270376686, + "mean": 6.97353811209655, + "stddev": 2.28656463030412, + } + + self.assertRasterFitsUnivar( + self.tci, + reference=reference, + precision=0.001, ) - def test_aFlag(self): - """Test the -a flag for positive flow accumulation""" + def test_spi(self): + """Test SPI calculation.""" self.assertModule( "r.watershed", - flags="a", elevation=self.elevation, threshold="10000", - accumulation=self.accumulation, + spi=self.spi, ) - # Verify output is created with -a flag - self.assertRasterExists( - self.accumulation, msg="Accumulation with -a flag not created" + + reference = { + "n": 2596072, + "null_cells": 22638, + "min": 0.000144249450029743, + "max": 1207802.52599239, + "mean": 73.7040561277494, + "stddev": 2750.87096268131, + } + + self.assertRasterFitsUnivar( + self.spi, + reference=reference, + precision=0.001, ) - def test_consistentResults(self): - """Test that running twice with same parameters gives identical results""" - # First run + def test_reproducibility(self): + """Test that multiple runs produce identical results""" self.assertModule( "r.watershed", elevation=self.elevation, threshold="10000", basin=self.basin, ) - # Store first result self.runModule("g.copy", raster=(self.basin, "basin_copy")) - # Second run with overwrite self.assertModule( "r.watershed", elevation=self.elevation, @@ -317,13 +415,13 @@ def test_consistentResults(self): basin=self.basin, overwrite=True, ) - # Results should be identical + self.assertRastersNoDifference( - self.basin, "basin_copy", 0, msg="Results are not reproducible" + self.basin, "basin_copy", precision=0, msg="Results are not reproducible" ) self.runModule("g.remove", flags="f", type="raster", name="basin_copy") - def test_minimumThreshold(self): + def test_minimum_threshold(self): """Test that minimum valid threshold (1) works""" self.assertModule( "r.watershed", @@ -331,22 +429,7 @@ def test_minimumThreshold(self): threshold="1", stream=self.stream, ) - self.assertRasterExists( - self.stream, msg="Stream output with threshold=1 not created" - ) - - def test_bFlag(self): - """Test the -b flag for beautification""" - self.assertModule( - "r.watershed", - flags="b", - elevation=self.elevation, - threshold="10000", - accumulation=self.accumulation, - ) - self.assertRasterExists( - self.accumulation, msg="Accumulation with -b flag not created" - ) + self.assertRasterExists(self.stream, msg="Stream with threshold=1 not created") if __name__ == "__main__": From 89d98f8a8ce43d6e8821522ad8f4293cb01ad8f6 Mon Sep 17 00:00:00 2001 From: Sumit Chintanwar Date: Sun, 8 Feb 2026 06:19:28 +0530 Subject: [PATCH 4/4] update reference values --- .../r.watershed/testsuite/r_watershed_test.py | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/raster/r.watershed/testsuite/r_watershed_test.py b/raster/r.watershed/testsuite/r_watershed_test.py index 05c37c5c83f..6381fbacdc7 100644 --- a/raster/r.watershed/testsuite/r_watershed_test.py +++ b/raster/r.watershed/testsuite/r_watershed_test.py @@ -167,12 +167,12 @@ def test_accumulation_mfd(self): ) reference = { - "n": 2602530, - "null_cells": 16180, - "min": -832420.692197234, - "max": 429067.069562766, - "mean": -343.747566806773, - "stddev": 16259.307703876, + "n": 2025000, + "null_cells": 0, + "min": -638532.804697762, + "max": 330838.090289589, + "mean": -262.230525740842, + "stddev": 13021.2714575589, } self.assertRasterFitsUnivar( @@ -192,12 +192,12 @@ def test_accumulation_sfd(self): ) reference = { - "n": 2602530, - "null_cells": 16180, - "min": -832531, - "max": 441312, - "mean": -287.797973894633, - "stddev": 16793.5124227608, + "n": 2025000, + "null_cells": 0, + "min": -638659, + "max": 332088, + "mean": -211.916512098765, + "stddev": 13234.9430084911, } self.assertRasterFitsUnivar( @@ -216,12 +216,12 @@ def test_basin_threshold_10k(self): ) reference = { - "n": 2456668, - "null_cells": 162042, + "n": 1879336, + "null_cells": 145664, "min": 2, - "max": 274, - "mean": 142.826128723946, - "stddev": 86.332946971229, + "max": 256, + "mean": 123.411570895252, + "stddev": 83.5230038874193, } self.assertRasterFitsUnivar( @@ -240,12 +240,12 @@ def test_basin_threshold_100k(self): ) reference = { - "n": 2026515, - "null_cells": 592195, + "n": 1554577, + "null_cells": 470423, "min": 2, - "max": 20, - "mean": 12.6790475274054, - "stddev": 5.81111419543262, + "max": 12, + "mean": 7.06497651772797, + "stddev": 3.5766222698306, } self.assertRasterFitsUnivar( @@ -264,12 +264,12 @@ def test_stream_network(self): ) reference = { - "n": 15740, - "null_cells": 2602970, + "n": 12656, + "null_cells": 2012344, "min": 2, - "max": 274, - "mean": 141.941041931385, - "stddev": 83.9583249945486, + "max": 256, + "mean": 122.276074589128, + "stddev": 79.4923046646631, } self.assertRasterFitsUnivar( @@ -288,12 +288,12 @@ def test_half_basin(self): ) reference = { - "n": 2456668, - "null_cells": 162042, + "n": 1879336, + "null_cells": 145664, "min": 1, - "max": 274, - "mean": 142.308489384809, - "stddev": 86.3166284098813, + "max": 256, + "mean": 122.882656959692, + "stddev": 83.5268097330046, } self.assertRasterFitsUnivar( @@ -312,12 +312,12 @@ def test_slope_steepness(self): ) reference = { - "n": 2602530, - "null_cells": 16180, + "n": 2025000, + "null_cells": 0, "min": 0.03, - "max": 3.07919076172568, - "mean": 0.153479116301884, - "stddev": 0.163185729045392, + "max": 4.41823404686636, + "mean": 0.156939437643538, + "stddev": 0.20267971226026, } self.assertRasterFitsUnivar( @@ -336,12 +336,12 @@ def test_length_slope(self): ) reference = { - "n": 2602530, - "null_cells": 16180, + "n": 2025000, + "null_cells": 0, "min": 0.03, - "max": 5.98881244191164, - "mean": 0.192025694191372, - "stddev": 0.240997329983397, + "max": 7.68844387178161, + "mean": 0.200451120710677, + "stddev": 0.296414490325605, } self.assertRasterFitsUnivar( @@ -360,12 +360,12 @@ def test_tci(self): ) reference = { - "n": 2596072, - "null_cells": 22638, - "min": 1.94904979310483, - "max": 26.8104270376686, - "mean": 6.97353811209655, - "stddev": 2.28656463030412, + "n": 2019304, + "null_cells": 5696, + "min": 1.1460354442537, + "max": 26.8009283618635, + "mean": 6.82155488487583, + "stddev": 2.63011648211108, } self.assertRasterFitsUnivar( @@ -384,12 +384,12 @@ def test_spi(self): ) reference = { - "n": 2596072, - "null_cells": 22638, + "n": 2019304, + "null_cells": 5696, "min": 0.000144249450029743, - "max": 1207802.52599239, - "mean": 73.7040561277494, - "stddev": 2750.87096268131, + "max": 1286492.83164802, + "mean": 115.490128734994, + "stddev": 4273.12317493444, } self.assertRasterFitsUnivar(