Merge branch 'release/3.16.0' into enhancement/1044-ndr-n-load-update

This commit is contained in:
Doug 2025-05-23 16:24:07 -04:00 committed by GitHub
commit 80763c7c6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 48 additions and 14 deletions

View File

@ -14,7 +14,7 @@ conda:
environment: .readthedocs_environment.yml
build:
os: ubuntu-22.04
os: ubuntu-24.04
tools:
python: "mambaforge-4.10"
jobs:

View File

@ -99,6 +99,8 @@ NDR
``measured-runoff``. See the Data Needs section of the NDR User
Guide for more details.
(`#1044 <https://github.com/natcap/invest/issues/1044>`_).
* Fixed a bug where input rasters (e.g. LULC) without a defined nodata value could
cause an OverflowError. (`#1904 <https://github.com/natcap/invest/issues/1904>`_).
Seasonal Water Yield
====================

View File

@ -9,6 +9,7 @@ import pygeoprocessing
import pygeoprocessing.routing
import taskgraph
from osgeo import gdal
from osgeo import gdal_array
from osgeo import ogr
from .. import gettext
@ -696,7 +697,7 @@ def execute(args):
'mask_raster_path': f_reg['mask_path'],
'target_masked_raster_path': f_reg['masked_runoff_proxy_path'],
'target_dtype': gdal.GDT_Float32,
'default_nodata': _TARGET_NODATA,
'target_nodata': _TARGET_NODATA,
},
dependent_task_list=[mask_task, align_raster_task],
target_path_list=[f_reg['masked_runoff_proxy_path']],
@ -709,7 +710,7 @@ def execute(args):
'mask_raster_path': f_reg['mask_path'],
'target_masked_raster_path': f_reg['masked_dem_path'],
'target_dtype': gdal.GDT_Float32,
'default_nodata': float(numpy.finfo(numpy.float32).min),
'target_nodata': float(numpy.finfo(numpy.float32).min),
},
dependent_task_list=[mask_task, align_raster_task],
target_path_list=[f_reg['masked_dem_path']],
@ -722,7 +723,7 @@ def execute(args):
'mask_raster_path': f_reg['mask_path'],
'target_masked_raster_path': f_reg['masked_lulc_path'],
'target_dtype': gdal.GDT_Int32,
'default_nodata': numpy.iinfo(numpy.int32).min,
'target_nodata': numpy.iinfo(numpy.int32).min,
},
dependent_task_list=[mask_task, align_raster_task],
target_path_list=[f_reg['masked_lulc_path']],
@ -1207,7 +1208,7 @@ def _create_mask_raster(source_raster_path, source_vector_path,
def _mask_raster(source_raster_path, mask_raster_path,
target_masked_raster_path, default_nodata, target_dtype):
target_masked_raster_path, target_nodata, target_dtype):
"""Using a raster of 1s and 0s, determine which pixels remain in output.
Args:
@ -1219,8 +1220,8 @@ def _mask_raster(source_raster_path, mask_raster_path,
target raster.
target_masked_raster_path (str): The path to where the target raster
should be written.
default_nodata (int, float, None): The nodata value that should be used
if ``source_raster_path`` does not have a defined nodata value.
target_nodata (int, float): The target nodata value that should match
``target_dtype``.
target_dtype (int): The ``gdal.GDT_*`` datatype of the target raster.
Returns:
@ -1228,22 +1229,20 @@ def _mask_raster(source_raster_path, mask_raster_path,
"""
source_raster_info = pygeoprocessing.get_raster_info(source_raster_path)
source_nodata = source_raster_info['nodata'][0]
nodata = source_nodata
if nodata is None:
nodata = default_nodata
target_numpy_dtype = gdal_array.GDALTypeCodeToNumericTypeCode(target_dtype)
def _mask_op(mask, raster):
result = numpy.full(mask.shape, nodata,
dtype=source_raster_info['numpy_type'])
result = numpy.full(mask.shape, target_nodata,
dtype=target_numpy_dtype)
valid_pixels = (
~pygeoprocessing.array_equals_nodata(raster, nodata) &
~pygeoprocessing.array_equals_nodata(raster, source_nodata) &
(mask == 1))
result[valid_pixels] = raster[valid_pixels]
return result
pygeoprocessing.raster_calculator(
[(mask_raster_path, 1), (source_raster_path, 1)], _mask_op,
target_masked_raster_path, target_dtype, nodata)
target_masked_raster_path, target_dtype, target_nodata)
def _add_fields_to_shapefile(field_pickle_map, target_vector_path):

View File

@ -370,6 +370,39 @@ class NDRTests(unittest.TestCase):
if mismatch_list:
raise RuntimeError("results not expected: %s" % mismatch_list)
def test_mask_raster_nodata_overflow(self):
"""NDR test when target nodata value overflows source dtype."""
from natcap.invest.ndr import ndr
source_raster_path = os.path.join(self.workspace_dir, 'source.tif')
target_raster_path = os.path.join(
self.workspace_dir, 'target.tif')
source_dtype = numpy.int8
target_dtype = gdal.GDT_Int32
target_nodata = numpy.iinfo(numpy.int32).min
pygeoprocessing.numpy_array_to_raster(
base_array=numpy.full((4, 4), 1, dtype=source_dtype),
target_nodata=None,
pixel_size=(1, -1),
origin=(0, 0),
projection_wkt=None,
target_path=source_raster_path)
ndr._mask_raster(
source_raster_path=source_raster_path,
mask_raster_path=source_raster_path, # mask=source for convenience
target_masked_raster_path=target_raster_path,
target_nodata=target_nodata,
target_dtype=target_dtype)
# Mostly we're testing that _mask_raster did not raise an OverflowError,
# but we can assert the results anyway.
array = pygeoprocessing.raster_to_numpy_array(target_raster_path)
numpy.testing.assert_array_equal(
array,
numpy.full((4, 4), 1, dtype=numpy.int32)) # matches target_dtype
def test_validation(self):
"""NDR test argument validation."""
from natcap.invest import validation