diff --git a/.github/actions/setup_env/action.yml b/.github/actions/setup_env/action.yml index 2678553f17..8b2b3a4b43 100644 --- a/.github/actions/setup_env/action.yml +++ b/.github/actions/setup_env/action.yml @@ -60,7 +60,7 @@ runs: cat environment.yml - name: Setup conda environment - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v2 with: environment-file: environment.yml environment-name: env diff --git a/HISTORY.rst b/HISTORY.rst index abdfee9f61..e1ae72528c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -54,6 +54,8 @@ Unreleased Changes * Fixed an issue on Windows where GDAL fails to find its DLLs due to an interfering GDAL installation on the PATH, such as from anaconda. https://github.com/natcap/invest/issues/1643 + * Improved error handling of NA values in raster reclassification to provide + a more descriptive message. * Workbench * Several small updates to the model input form UI to improve usability and visual consistency (https://github.com/natcap/invest/issues/912). @@ -81,6 +83,8 @@ Unreleased Changes (https://github.com/natcap/invest/issues/1615). * Rarity values are now output in CSV format (as well as in raster format) (https://github.com/natcap/invest/issues/721). + * Improved error handling when there is a missing LULC value in the + sensitivity table (https://github.com/natcap/invest/issues/1671). * Pollination * Fixed an issue with nodata handling that was causing some outputs to be filled either with the float32 value for positive infinity, or else with diff --git a/requirements-dev.txt b/requirements-dev.txt index 07ae8b13ab..28b33c0f05 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,7 +14,7 @@ virtualenv>=12.0.1 pytest -pytest-subtests<0.14.0 +pytest-subtests!=0.14.0 # https://github.com/pytest-dev/pytest-subtests/issues/173 wheel>=0.27.0 pypiwin32; sys_platform == 'win32' # pip-only diff --git a/src/natcap/invest/urban_nature_access.py b/src/natcap/invest/urban_nature_access.py index 56c12ebae5..ed824412e7 100644 --- a/src/natcap/invest/urban_nature_access.py +++ b/src/natcap/invest/urban_nature_access.py @@ -59,7 +59,8 @@ 'projection_units': u.meter, 'about': ( "A map of LULC codes. " - "All values in this raster must have corresponding entries " + "Each land use/land cover type must be assigned a unique integer " + "code. All values in this raster must have corresponding entries " "in the LULC attribute table. For this model in particular, " "the urban nature types are of importance. Non-nature types " "are not required to be uniquely identified. All outputs " diff --git a/src/natcap/invest/utils.py b/src/natcap/invest/utils.py index dff0825a13..98a14e411e 100644 --- a/src/natcap/invest/utils.py +++ b/src/natcap/invest/utils.py @@ -741,14 +741,24 @@ def reclassify_raster( None Raises: - ValueError if ``values_required`` is ``True`` and a pixel value from - ``raster_path_band`` is not a key in ``value_map``. + ValueError: + - if ``values_required`` is ``True`` and a pixel value from + ``raster_path_band`` is not a key in ``value_map``. + TypeError: + - if there is a ``None`` or ``NA`` key in ``value_map``. """ # Error early if 'error_details' keys are invalid raster_name = error_details['raster_name'] column_name = error_details['column_name'] table_name = error_details['table_name'] + # check keys in value map to ensure none are NA or None + if any((key is pandas.NA or key is None) + for key in value_map): + error_message = (f"Missing or NA value in '{column_name}' column" + f" in {table_name} table.") + raise TypeError(error_message) + try: pygeoprocessing.reclassify_raster( raster_path_band, value_map, target_raster_path, target_datatype, diff --git a/tests/test_habitat_quality.py b/tests/test_habitat_quality.py index 143d113668..db5d6b5d59 100644 --- a/tests/test_habitat_quality.py +++ b/tests/test_habitat_quality.py @@ -2133,3 +2133,48 @@ def test_habitat_quality_validate_missing_fut_column(self): header='column', header_name='fut_path') )] self.assertEqual(validate_result, expected) + + def test_habitat_quality_missing_lulc_val_in_sens_table(self): + """Habitat Quality: test for empty value in LULC column of + sensitivity table. Expects TypeError""" + from natcap.invest import habitat_quality + + args = { + 'half_saturation_constant': '0.5', + 'workspace_dir': self.workspace_dir, + 'n_workers': -1, + } + + lulc_array = numpy.ones((100, 100), dtype=numpy.int8) + args['lulc_cur_path'] = os.path.join( + args['workspace_dir'], 'lc_samp_cur_b.tif') + make_raster_from_array( + lulc_array, args['lulc_cur_path']) + + args['sensitivity_table_path'] = os.path.join( + args['workspace_dir'], 'sensitivity_samp.csv') + with open(args['sensitivity_table_path'], 'w') as open_table: + open_table.write('LULC,NAME,HABITAT,threat_1,threat_2\n') + open_table.write('1,"lulc 1",1,1,1\n') + open_table.write(',"lulc 2",0.5,0.5,1\n') # missing LULC value + open_table.write('3,"lulc 3",0,0.3,1\n') + + make_threats_raster( + args['workspace_dir'], threat_values=[1, 1], + dtype=numpy.int8, gdal_type=gdal.GDT_Int32, nodata_val=None) + + args['threats_table_path'] = os.path.join( + args['workspace_dir'], 'threats_samp.csv') + + # create the threat CSV table + with open(args['threats_table_path'], 'w') as open_table: + open_table.write( + 'MAX_DIST,WEIGHT,THREAT,DECAY,BASE_PATH,CUR_PATH,FUT_PATH\n') + open_table.write( + '0.04,0.7,threat_1,linear,,threat_1_c.tif,threat_1_f.tif\n') + open_table.write( + '0.07,1.0,threat_2,exponential,,threat_2_c.tif,' + 'threat_2_f.tif\n') + + with self.assertRaises(TypeError): + habitat_quality.execute(args)