From f1de1e0e35da235ec76ab085c74819c173ed7adc Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Thu, 20 Jan 2022 22:07:41 -0700 Subject: [PATCH] FIX: Add clip limits to Nightshade floating point calculations Depending on the values input to Nightshade, the floating point precision could put the range for arccos outside of [-1, 1], which in turn numpy returns as nan's, yielding bad geometries further downstream. This patch clips the arccos calculations to [-1, 1] to guarantee we aren't out of the valid floating point bounds. --- lib/cartopy/feature/nightshade.py | 8 ++++++-- lib/cartopy/tests/feature/test_nightshade.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/cartopy/feature/nightshade.py b/lib/cartopy/feature/nightshade.py index b8a613474..8a8f28858 100644 --- a/lib/cartopy/feature/nightshade.py +++ b/lib/cartopy/feature/nightshade.py @@ -77,8 +77,12 @@ def __init__(self, date=None, delta=0.1, refraction=-0.83, # Solve the generalized equation for omega0, which is the # angle of sunrise/sunset from solar noon - omega0 = np.rad2deg(np.arccos(np.sin(np.deg2rad(refraction)) / - np.cos(np.deg2rad(y)))) + # We need to clip the input to arccos to [-1, 1] due to floating + # point precision and arccos creating nans for values outside + # of the domain + arccos_tmp = np.clip(np.sin(np.deg2rad(refraction)) / + np.cos(np.deg2rad(y)), -1, 1) + omega0 = np.rad2deg(np.arccos(arccos_tmp)) # Fill the longitude values from the offset for midnight. # This needs to be a closed loop to fill the polygon. diff --git a/lib/cartopy/tests/feature/test_nightshade.py b/lib/cartopy/tests/feature/test_nightshade.py index 8764943df..062f91eab 100644 --- a/lib/cartopy/tests/feature/test_nightshade.py +++ b/lib/cartopy/tests/feature/test_nightshade.py @@ -8,7 +8,7 @@ import pytest -from cartopy.feature.nightshade import _julian_day, _solar_position +from cartopy.feature.nightshade import _julian_day, _solar_position, Nightshade def test_julian_day(): @@ -44,3 +44,12 @@ def test_solar_position(dt, true_lat, true_lon): lat, lon = _solar_position(dt) assert pytest.approx(true_lat, 0.1) == lat assert pytest.approx(true_lon, 0.1) == lon + + +def test_nightshade_floating_point(): + # Smoke test for clipping nightshade floating point values + date = datetime(1999, 12, 31, 12) + + # This can cause an error with floating point precision if it is + # set to exactly -6 and arccos input is not clipped to [-1, 1] + Nightshade(date, refraction=-6.0, color='none')