From 3294e6a1020939b6f442eae2750462a4698495ce Mon Sep 17 00:00:00 2001 From: James Douglass Date: Thu, 19 Sep 2024 15:50:47 -0700 Subject: [PATCH 1/5] Implementing a function to guarantee that the LULC has a nodata value. --- src/natcap/invest/urban_nature_access.py | 43 ++++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/natcap/invest/urban_nature_access.py b/src/natcap/invest/urban_nature_access.py index 2831e6528..35fde6188 100644 --- a/src/natcap/invest/urban_nature_access.py +++ b/src/natcap/invest/urban_nature_access.py @@ -755,13 +755,12 @@ def execute(args): squared_lulc_pixel_size = _square_off_pixels(args['lulc_raster_path']) lulc_alignment_task = graph.add_task( - pygeoprocessing.warp_raster, + _warp_lulc, kwargs={ - 'base_raster_path': args['lulc_raster_path'], - 'target_pixel_size': squared_lulc_pixel_size, - 'target_bb': target_bounding_box, - 'target_raster_path': file_registry['aligned_lulc'], - 'resample_method': 'near', + "source_lulc_path": args['lulc_raster_path'], + "target_lulc_path": file_registry['aligned_lulc'], + "target_pixel_size": squared_lulc_pixel_size, + "target_bounding_box": target_bounding_box, }, target_path_list=[file_registry['aligned_lulc']], task_name='Resample LULC to have square pixels' @@ -2528,6 +2527,38 @@ def _create_mask(*raster_arrays): _create_mask, target_mask_path, gdal.GDT_Byte, nodata_target=255) +def _warp_lulc(source_lulc_path, target_lulc_path, target_pixel_size, + target_bounding_box): + """Warp a LULC raster and set a nodata if needed. + + Args: + source_lulc_path (str): The path to a source LULC raster. + target_lulc_path (str): The path to the new LULC raster. + target_pixel_size (tuple): A 2-tuple of the target pixel size. + target_bounding_box (tuple): A 4-tuple of the target bounding box. + + Returns: + ``None``. + """ + source_raster_info = pygeoprocessing.get_raster_info(source_lulc_path) + target_nodata = source_raster_info['nodata'][0] + if target_nodata is None: + target_nodata = pygeoprocessing.choose_nodata( + source_raster_info['numpy_type']) + + pygeoprocessing.warp_raster( + source_lulc_path, target_pixel_size, target_lulc_path, + 'near', target_bb=target_bounding_box, + target_projection_wkt=source_raster_info['projection_wkt']) + + # if there is no defined nodata, set a default value + raster = gdal.OpenEx(target_lulc_path, gdal.GA_Update) + band = raster.GetRasterBand(1) + band.SetNoDataValue(target_nodata) + band = None + raster = None + + def _mask_raster(source_raster_path, mask_raster_path, target_raster_path): """Convert pixels to nodata given an existing mask raster. From eca7a62512b5dade74903a6ff6f8743df6eaa117 Mon Sep 17 00:00:00 2001 From: James Douglass Date: Thu, 19 Sep 2024 15:59:37 -0700 Subject: [PATCH 2/5] Guaranteeing that the nodata value is out of range. RE:#1293 --- src/natcap/invest/urban_nature_access.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/natcap/invest/urban_nature_access.py b/src/natcap/invest/urban_nature_access.py index 35fde6188..c8b492869 100644 --- a/src/natcap/invest/urban_nature_access.py +++ b/src/natcap/invest/urban_nature_access.py @@ -2543,8 +2543,10 @@ def _warp_lulc(source_lulc_path, target_lulc_path, target_pixel_size, source_raster_info = pygeoprocessing.get_raster_info(source_lulc_path) target_nodata = source_raster_info['nodata'][0] if target_nodata is None: + # Guarantee that our nodata cannot be represented by the datatype - + # select a nodata value that's out of range. target_nodata = pygeoprocessing.choose_nodata( - source_raster_info['numpy_type']) + source_raster_info['numpy_type']) + 1 pygeoprocessing.warp_raster( source_lulc_path, target_pixel_size, target_lulc_path, From b6339e6e5cbd32431255c0b2576586110c11e2a9 Mon Sep 17 00:00:00 2001 From: James Douglass Date: Thu, 19 Sep 2024 16:02:40 -0700 Subject: [PATCH 3/5] Noting change in HISTORY. RE:#1293 --- HISTORY.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index dfa49b302..c888d1bfd 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -37,6 +37,10 @@ Unreleased Changes ------------------ +* Urban Nature Access + * The model now works as expected when the user provides an LULC raster + that does not have a nodata value defined. + https://github.com/natcap/invest/issues/1293 * Workbench * Several small updates to the model input form UI to improve usability and visual consistency (https://github.com/natcap/invest/issues/912) From e6eb1a037a28c8c56adc633425dc8bb467e83ad8 Mon Sep 17 00:00:00 2001 From: James Douglass Date: Thu, 19 Sep 2024 16:06:24 -0700 Subject: [PATCH 4/5] Adding a test for when the LULC has no nodata value. RE:#1293 --- tests/test_urban_nature_access.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_urban_nature_access.py b/tests/test_urban_nature_access.py index 0e4858a83..4e5141f0b 100644 --- a/tests/test_urban_nature_access.py +++ b/tests/test_urban_nature_access.py @@ -358,6 +358,21 @@ def test_core_model(self): self.assertAlmostEqual(numpy.min(valid_pixels), 1171.7352294921875) self.assertAlmostEqual(numpy.max(valid_pixels), 11898.0712890625) + def test_no_lulc_nodata(self): + """UNA: verify behavior when the LULC has no nodata value.""" + from natcap.invest import urban_nature_access + + args = _build_model_args(self.workspace_dir) + args['search_radius_mode'] = urban_nature_access.RADIUS_OPT_UNIFORM + args['search_radius'] = 100 + + raster = gdal.OpenEx(args['lulc_raster_path'], gdal.OF_RASTER) + band = raster.GetRasterBand(1) + band.DeleteNoDataValue() + band = None + raster = None + urban_nature_access.execute(args) + def test_split_urban_nature(self): from natcap.invest import urban_nature_access From 94f7f006e88c277362f33ff86a0d89cc037564ea Mon Sep 17 00:00:00 2001 From: James Douglass Date: Tue, 1 Oct 2024 14:34:39 -0700 Subject: [PATCH 5/5] Cleaning up a conditional. RE:#1293 --- src/natcap/invest/urban_nature_access.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/natcap/invest/urban_nature_access.py b/src/natcap/invest/urban_nature_access.py index c8b492869..136575a95 100644 --- a/src/natcap/invest/urban_nature_access.py +++ b/src/natcap/invest/urban_nature_access.py @@ -2542,11 +2542,6 @@ def _warp_lulc(source_lulc_path, target_lulc_path, target_pixel_size, """ source_raster_info = pygeoprocessing.get_raster_info(source_lulc_path) target_nodata = source_raster_info['nodata'][0] - if target_nodata is None: - # Guarantee that our nodata cannot be represented by the datatype - - # select a nodata value that's out of range. - target_nodata = pygeoprocessing.choose_nodata( - source_raster_info['numpy_type']) + 1 pygeoprocessing.warp_raster( source_lulc_path, target_pixel_size, target_lulc_path, @@ -2554,11 +2549,16 @@ def _warp_lulc(source_lulc_path, target_lulc_path, target_pixel_size, target_projection_wkt=source_raster_info['projection_wkt']) # if there is no defined nodata, set a default value - raster = gdal.OpenEx(target_lulc_path, gdal.GA_Update) - band = raster.GetRasterBand(1) - band.SetNoDataValue(target_nodata) - band = None - raster = None + if target_nodata is None: + # Guarantee that our nodata cannot be represented by the datatype - + # select a nodata value that's out of range. + target_nodata = pygeoprocessing.choose_nodata( + source_raster_info['numpy_type']) + 1 + raster = gdal.OpenEx(target_lulc_path, gdal.GA_Update) + band = raster.GetRasterBand(1) + band.SetNoDataValue(target_nodata) + band = None + raster = None def _mask_raster(source_raster_path, mask_raster_path, target_raster_path):