Repository: SciTools/cartopy

Issue:
Nightshade w/ refraction=-6.0 yields ValueError: GEOSGeom_createLinearRing_r returned a NULL pointer
### Description

I'm trying use Nightshade to draw twilight lines (and sub solar point).

If I do Nightshade w/ refraction=-6.0, I get ValueError: GEOSGeom_createLinearRing_r returned a NULL pointer

Nightshade w/ refraction=-6.0001 works OK

Also, refraction=89 causes extraneous lines.

#### Code to reproduce

```
import matplotlib.pyplot as plt
fig = plt.figure()

import cartopy
ax = fig.add_subplot(projection=cartopy.crs.PlateCarree())
ax.coastlines()

import datetime
date = datetime.datetime(1999, 12, 31, 12)

from cartopy.feature.nightshade import Nightshade

ax.add_feature(Nightshade(date, color='none'))
ax.add_feature(Nightshade(date, refraction=-6.0, color='none')) # Civil Twilight
#ax.add_feature(Nightshade(date, refraction=-6.0001, color='none')) # Civil Twilight
ax.add_feature(Nightshade(date, refraction=-12, color='none')) # Nautical Twilight

ax.add_feature(Nightshade(date, refraction=89, color='none')) # sub solar circle ...

plt.show()
```

#### Traceback 

```
python /tmp/nightshade-bug.py 
/usr/lib64/python3.10/site-packages/cartopy/feature/nightshade.py:80: RuntimeWarning: invalid value encountered in arccos
  omega0 = np.rad2deg(np.arccos(np.sin(np.deg2rad(refraction)) /
IllegalArgumentException: Points of LinearRing do not form a closed linestring
Traceback (most recent call last):
  File "/tmp/nightshade-bug.py", line 15, in <module>
    ax.add_feature(Nightshade(date, refraction=-6, color='none')) # Civil Twilight
  File "/usr/lib64/python3.10/site-packages/cartopy/feature/nightshade.py", line 93, in __init__
    geom = sgeom.Polygon(np.column_stack((x, y)))
  File "/usr/lib64/python3.10/site-packages/shapely/geometry/polygon.py", line 243, in __init__
    ret = geos_polygon_from_py(shell, holes)
  File "/usr/lib64/python3.10/site-packages/shapely/geometry/polygon.py", line 509, in geos_polygon_from_py
    ret = geos_linearring_from_py(shell)
  File "shapely/speedups/_speedups.pyx", line 408, in shapely.speedups._speedups.geos_linearring_from_py
ValueError: GEOSGeom_createLinearRing_r returned a NULL pointer
```

<details>
 <summary>Full environment definition</summary>

<!-- fill in the following information as appropriate -->

### Operating system

Fedora 35

### Cartopy version

python3-cartopy-0.20.1-1.fc35.x86_64

python3-shapely-1.7.1-12.fc35.x86_64
python3-matplotlib-3.5.1-3.fc35.x86_64

</details>

Thanks !


Discussion:

Resolving patch:
diff --git a/lib/cartopy/feature/nightshade.py b/lib/cartopy/feature/nightshade.py
--- 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.


Test patch:
diff --git a/lib/cartopy/tests/feature/test_nightshade.py b/lib/cartopy/tests/feature/test_nightshade.py
--- 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')