From 02fd692c43a7344b00ace217b91f1134f9230d5f Mon Sep 17 00:00:00 2001 From: Claire Simpson Date: Fri, 19 Jun 2026 14:47:18 -0600 Subject: [PATCH 1/4] Add special value tests; fix pixel dims #2593 --- tests/reports/test_raster_utils.py | 119 ++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/tests/reports/test_raster_utils.py b/tests/reports/test_raster_utils.py index a6dc30516b..c9bdef3300 100644 --- a/tests/reports/test_raster_utils.py +++ b/tests/reports/test_raster_utils.py @@ -49,7 +49,7 @@ def save_figure(fig, filepath): def make_simple_raster(target_filepath, shape): array = numpy.linspace(0, 1, num=numpy.multiply(*shape)).reshape(*shape) pygeoprocessing.numpy_array_to_raster( - array, target_nodata=None, pixel_size=(1, 1), origin=(0, 0), + array, target_nodata=None, pixel_size=(1, -1), origin=(0, 0), projection_wkt=PROJ_WKT, target_path=target_filepath) @@ -325,6 +325,18 @@ def test_plot_raster_list_different_transforms(self): save_figure(fig, actual_png) compare_snapshots(reference, actual_png) + +class RasterSpecialValueConfigTests(unittest.TestCase): + """Snapshot tests for special values in RasterConfig.""" + + def setUp(self): + """Override setUp function to create temp workspace directory.""" + self.workspace_dir = tempfile.mkdtemp() + + def tearDown(self): + """Override tearDown function to remove temporary directory.""" + shutil.rmtree(self.workspace_dir) + def test_special_value_config(self): """Should pass when only index 0 (lower bound) is fully populated.""" config = SpecialValueConfig( @@ -361,6 +373,111 @@ def test_special_value_config(self): "If index 1 is `None` in any of the special config tuples" in str(context.exception)) + def test_special_values_rejected_for_nominal(self): + """RasterPlotConfig does not allow special values for nominal raster""" + with self.assertRaisesRegex( + ValueError, '`special_values` may only be defined'): + raster_utils.RasterPlotConfig( + raster_path=os.path.join(self.workspace_dir, 'foo.tif'), + datatype=raster_utils.RasterDatatype.nominal, + spec=spec.Output(id='foo', about='foo output'), + special_values=raster_utils.SpecialValueConfig( + thresholds=(-1, 1), + labels=('low', 'high'), + colors=('red', 'blue'))) + + def test_special_values_rejected_for_binary(self): + """RasterPlotConfig does not allow special values for binary raster""" + with self.assertRaisesRegex( + ValueError, '`special_values` may only be defined'): + raster_utils.RasterPlotConfig( + raster_path=os.path.join(self.workspace_dir, 'foo.tif'), + datatype=raster_utils.RasterDatatype.binary, + spec=spec.Output(id='foo', about='foo output'), + special_values=raster_utils.SpecialValueConfig( + thresholds=(-1, 1), + labels=('low', 'high'), + colors=('red', 'blue'))) + + def test_configure_special_values_both_bounds(self): + """_configure_special_values configures both colorbar extensions.""" + cmap = matplotlib.colormaps['viridis'].copy() + special_values = raster_utils.SpecialValueConfig( + thresholds=(-1, 1), + labels=('low', 'high'), + colors=('red', 'blue')) + + extend, thresholds, labels, text_specs = ( + raster_utils._configure_special_values(cmap, special_values)) + + self.assertEqual(extend, 'both') + self.assertEqual(thresholds, [-1, 1]) + self.assertEqual(labels, ['low', 'high']) + self.assertEqual( + text_specs, [(0, -0.05, 'top'), (0, 1.05, 'bottom')]) + + def test_plot_divergent_log_raster_requires_symmetric_thresholds( + self): + """Divergent log special values must be symmetric around 0.""" + shape = (4, 4) + raster_config = raster_utils.RasterPlotConfig( + raster_path=os.path.join(self.workspace_dir, 'foo.tif'), + datatype=raster_utils.RasterDatatype.divergent, + transform="log", + spec=spec.Output(id='foo', about='foo output'), + special_values=raster_utils.SpecialValueConfig( + thresholds=(0.4, 1), + labels=('low', 'high'), + colors=('black', 'orange'))) + make_simple_raster(raster_config.raster_path, shape) + + with self.assertRaisesRegex( + UserWarning, 'To ensure that 0 falls at the logical break'): + raster_utils.plot_raster_list([raster_config]) + + def test_plot_continuous_raster_special_values(self): + """Test correct plot for continuous raster with special values""" + figname = 'plot_raster_list_special_values.png' + reference = os.path.join(REFS_DIR, figname) + shape = (4, 4) + raster_config = raster_utils.RasterPlotConfig( + raster_path=os.path.join(self.workspace_dir, 'foo.tif'), + datatype=raster_utils.RasterDatatype.continuous, + spec=spec.Output(id='foo', about='foo output'), + special_values=raster_utils.SpecialValueConfig( + thresholds=(0.4, 1), + labels=('low', 'high'), + colors=('black', 'orange'))) + make_simple_raster(raster_config.raster_path, shape) + + config_list = [raster_config] + fig = raster_utils.plot_raster_list(config_list) + actual_png = os.path.join(self.workspace_dir, figname) + save_figure(fig, actual_png) + compare_snapshots(reference, actual_png) + + def test_plot_divergent_raster_max_special_value(self): + """Test divergent raster plot w special value has a correct colorbar""" + figname = 'plot_raster_list_special_max_value.png' + reference = os.path.join(REFS_DIR, figname) + shape = (4, 4) + raster_config = raster_utils.RasterPlotConfig( + raster_path=os.path.join(self.workspace_dir, 'foo.tif'), + datatype=raster_utils.RasterDatatype.divergent, + spec=spec.Output(id='foo', about='foo output'), + special_values=raster_utils.SpecialValueConfig( + thresholds=(None, 0.8), + labels=(None, 'high'), + colors=(None, 'darkblue'))) + # Note this raster doesn't actually have negative values + make_simple_raster(raster_config.raster_path, shape) + + config_list = [raster_config] + fig = raster_utils.plot_raster_list(config_list) + actual_png = os.path.join(self.workspace_dir, figname) + save_figure(fig, actual_png) + compare_snapshots(reference, actual_png) + class RasterPlotLegendTests(unittest.TestCase): """Snapshot tests for legend placement on nominal rasters.""" From d2b0ea24d9794715fc3592a55470260f632db883 Mon Sep 17 00:00:00 2001 From: Claire Simpson Date: Fri, 19 Jun 2026 14:48:43 -0600 Subject: [PATCH 2/4] Fix deprecation warning by using cmap.with_extremes #2593 --- src/natcap/invest/reports/raster_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/natcap/invest/reports/raster_utils.py b/src/natcap/invest/reports/raster_utils.py index 8220bf59bd..eca861d120 100644 --- a/src/natcap/invest/reports/raster_utils.py +++ b/src/natcap/invest/reports/raster_utils.py @@ -419,14 +419,14 @@ def _configure_special_values( text_specs = [] if lower_threshold is not None: - cmap.set_under(lower_color) + cmap = cmap.with_extremes(under=lower_color) extend = 'min' thresholds.append(lower_threshold) labels.append(lower_label) text_specs.append((0, -0.05, 'top')) if upper_threshold is not None: - cmap.set_over(upper_color) + cmap = cmap.with_extremes(over=upper_color) extend = 'max' if extend == 'neither' else 'both' thresholds.append(upper_threshold) labels.append(upper_label) From a4606f94327354b18150f71271ddda9ffa88ed6e Mon Sep 17 00:00:00 2001 From: Claire Simpson Date: Fri, 19 Jun 2026 15:12:26 -0600 Subject: [PATCH 3/4] Add test for thresholds as ticks #2593 --- tests/reports/test_raster_utils.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/reports/test_raster_utils.py b/tests/reports/test_raster_utils.py index c9bdef3300..420d6fd39e 100644 --- a/tests/reports/test_raster_utils.py +++ b/tests/reports/test_raster_utils.py @@ -27,7 +27,7 @@ projection.ImportFromEPSG(3857) PROJ_WKT = projection.ExportToWkt() -REFS_DIR = os.path.join('data', 'invest-test-data', 'reports', 'snapshots') +REFS_DIR = os.path.join('../../data', 'invest-test-data', 'reports', 'snapshots') def setUpModule(): @@ -478,6 +478,27 @@ def test_plot_divergent_raster_max_special_value(self): save_figure(fig, actual_png) compare_snapshots(reference, actual_png) + def test_plot_raster_list_special_values_adds_threshold_ticks(self): + """Test plot_raster_list adds special values as colorbar ticks.""" + thresholds = (-.8, .9) + shape = (4, 4) + raster_config = raster_utils.RasterPlotConfig( + raster_path=os.path.join(self.workspace_dir, 'foo.tif'), + datatype=raster_utils.RasterDatatype.continuous, + spec=spec.Output(id='foo', about='foo output'), + special_values=raster_utils.SpecialValueConfig( + thresholds=thresholds, + labels=('low', 'high'), + colors=('red', 'blue'))) + make_simple_raster(raster_config.raster_path, shape) + + fig = raster_utils.plot_raster_list([raster_config]) + colorbar_ax = fig.axes[1] + ticks = list(colorbar_ax.get_yticks()) + + self.assertIn(thresholds[0], ticks) + self.assertIn(thresholds[1], ticks) + class RasterPlotLegendTests(unittest.TestCase): """Snapshot tests for legend placement on nominal rasters.""" From 1475373193250417214cd9fd07d090fddb398c11 Mon Sep 17 00:00:00 2001 From: Claire Simpson Date: Fri, 19 Jun 2026 15:19:32 -0600 Subject: [PATCH 4/4] Fix my temporary change to REFS_DIR #2593 --- tests/reports/test_raster_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/reports/test_raster_utils.py b/tests/reports/test_raster_utils.py index 420d6fd39e..3cf5ff3201 100644 --- a/tests/reports/test_raster_utils.py +++ b/tests/reports/test_raster_utils.py @@ -27,7 +27,7 @@ projection.ImportFromEPSG(3857) PROJ_WKT = projection.ExportToWkt() -REFS_DIR = os.path.join('../../data', 'invest-test-data', 'reports', 'snapshots') +REFS_DIR = os.path.join('data', 'invest-test-data', 'reports', 'snapshots') def setUpModule():