From 01565a689c60e995f17a6d20a38c13aba8ac1324 Mon Sep 17 00:00:00 2001 From: Benjamin Root Date: Wed, 8 Feb 2023 16:45:55 -0500 Subject: [PATCH 1/2] Force get_tolerance() to always be greater than zero. * Also move it from __main__.py to utils.py and add some tests --- src/climate_indices/__main__.py | 17 ++++++----------- src/climate_indices/utils.py | 11 +++++++++++ tests/test_utils.py | 23 +++++++++++++++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/climate_indices/__main__.py b/src/climate_indices/__main__.py index 1cdc6d9a..e81173ba 100644 --- a/src/climate_indices/__main__.py +++ b/src/climate_indices/__main__.py @@ -88,11 +88,6 @@ def _validate_args(args): ("division", "time"), ("division")] - # dynamic threshold absolute tolerance parameter np.allclose - # derived from (smallest) grid size along dimension dim - def get_tolerance(dim): - return np.diff(dim).min() / 10 - # all indices except PET require a precipitation file if args.index != "pet": @@ -218,11 +213,11 @@ def get_tolerance(dim): raise ValueError(msg) # verify that the coordinate variables match with those of the precipitation dataset - if not np.allclose(lats_precip, dataset_pet["lat"][:], atol=get_tolerance(lats_precip)): + if not np.allclose(lats_precip, dataset_pet["lat"][:], atol=utils.get_tolerance(lats_precip)): msg = "Precipitation and PET variables contain non-matching latitudes" _logger.error(msg) raise ValueError(msg) - elif not np.allclose(lons_precip, dataset_pet["lon"][:], atol=get_tolerance(lons_precip)): + elif not np.allclose(lons_precip, dataset_pet["lon"][:], atol=utils.get_tolerance(lons_precip)): msg = "Precipitation and PET variables contain non-matching longitudes" _logger.error(msg) raise ValueError(msg) @@ -300,11 +295,11 @@ def get_tolerance(dim): raise ValueError(msg) # verify that the coordinate variables match with those of the precipitation dataset - if not np.allclose(lats_precip, dataset_temp["lat"][:], atol=get_tolerance(lats_precip)): + if not np.allclose(lats_precip, dataset_temp["lat"][:], atol=utils.get_tolerance(lats_precip)): msg = "Precipitation and temperature variables contain non-matching latitudes" _logger.error(msg) raise ValueError(msg) - elif not np.allclose(lons_precip, dataset_temp["lon"][:], atol=get_tolerance(lons_precip)): + elif not np.allclose(lons_precip, dataset_temp["lon"][:], atol=utils.get_tolerance(lons_precip)): msg = "Precipitation and temperature variables contain non-matching longitudes" _logger.error(msg) raise ValueError(msg) @@ -378,11 +373,11 @@ def get_tolerance(dim): raise ValueError(msg) # verify that the coordinate variables match with those of the precipitation dataset - if not np.allclose(lats_precip, dataset_awc["lat"][:], atol=get_tolerance(lats_precip)): + if not np.allclose(lats_precip, dataset_awc["lat"][:], atol=utils.get_tolerance(lats_precip)): msg = "Precipitation and AWC variables contain non-matching latitudes" _logger.error(msg) raise ValueError(msg) - elif not np.allclose(lons_precip, dataset_awc["lon"][:], atol=get_tolerance(lons_precip)): + elif not np.allclose(lons_precip, dataset_awc["lon"][:], atol=utils.get_tolerance(lons_precip)): msg = "Precipitation and AWC variables contain non-matching longitudes" _logger.error(msg) raise ValueError(msg) diff --git a/src/climate_indices/utils.py b/src/climate_indices/utils.py index 32646386..e980e22d 100644 --- a/src/climate_indices/utils.py +++ b/src/climate_indices/utils.py @@ -489,4 +489,15 @@ def count_zeros_and_non_missings( return zeros, non_missings +# ------------------------------------------------------------------------------ +def get_tolerance(dim): + """ + dynamic threshold absolute tolerance parameter np.allclose + derived from (smallest) absolute grid size along dimension dim. + Always greater than zero. + """ + tol = np.abs(np.diff(dim).min() / 10) + return max(tol, np.finfo(tol.dtype).resolution) + + _logger = get_logger(__name__, logging.DEBUG) diff --git a/tests/test_utils.py b/tests/test_utils.py index 7a286855..9c2a1880 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -488,3 +488,26 @@ def test_transform_to_366day(): values_365, 1972, 24) + +def test_tolerance(): + lons, dlon = np.linspace(-180.0, 180.0, 250, retstep=True) + tol = utils.get_tolerance(lons) + assert tol > 0, "Tolerance must always be greater than zero" + assert tol < abs(dlon), "Tolerance must always come out smaller than the coordinate delta" + + lats, dlat = np.linspace(25.0, -15.0, 50, retstep=True) + tol = utils.get_tolerance(lats) + assert tol > 0, "Tolerance must always be greater than zero" + assert tol < abs(dlat), "Tolerance must always come out smaller than the coordinate delta" + + meshlat, meshlon = np.meshgrid(lats, lons) + tol = utils.get_tolerance(meshlat) + assert tol > 0, "Tolerance must always be greater than zero" + assert tol < abs(dlat), "Tolerance must always come out smaller than the coordinate delta" + # Tricky situation because np.diff() on this grid returns all zeros. + # Not the greatest situation, but we can at least allow a tiny bit of tolerance. + # Would be nice to be smarter about this situation. + tol = utils.get_tolerance(meshlon) + assert tol > 0, "Tolerance must always be greater than zero" + assert tol < abs(dlon), "Tolerance must always come out smaller than the coordinate delta" + From 612634c2935e83af24dbac4dd3ac2d47e26d2d55 Mon Sep 17 00:00:00 2001 From: Benjamin Root Date: Wed, 8 Feb 2023 17:05:53 -0500 Subject: [PATCH 2/2] Adding type hints for get_tolerance() --- src/climate_indices/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/climate_indices/utils.py b/src/climate_indices/utils.py index e980e22d..d9d2bda3 100644 --- a/src/climate_indices/utils.py +++ b/src/climate_indices/utils.py @@ -490,7 +490,7 @@ def count_zeros_and_non_missings( # ------------------------------------------------------------------------------ -def get_tolerance(dim): +def get_tolerance(dim: np.ndarray) -> float: """ dynamic threshold absolute tolerance parameter np.allclose derived from (smallest) absolute grid size along dimension dim.