Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions xrspatial/reproject/_crs_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"""
from __future__ import annotations

import numpy as np

from xrspatial.reproject._lite_crs import CRS as LiteCRS


Expand Down Expand Up @@ -111,9 +113,18 @@ def _detect_source_crs(raster):


def _detect_nodata(raster, nodata=None):
"""Determine nodata value from explicit arg, rioxarray, or attrs."""
"""Determine nodata value from explicit arg, rioxarray, or attrs.

NaN is the canonical sentinel for missing data and is allowed.
+/-Inf would break downstream `np.isnan` masks, so it is rejected.
"""
if nodata is not None:
return float(nodata)
nd = float(nodata)
if np.isinf(nd):
raise ValueError(
f"nodata must be finite or NaN, got {nodata!r}"
)
return nd

# rioxarray
try:
Expand Down
20 changes: 20 additions & 0 deletions xrspatial/reproject/_itrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,26 @@ def itrf_transform(lon, lat, h=0.0, *, src, tgt, epoch):
--------
>>> itrf_transform(-74.0, 40.7, 10.0, src='ITRF2014', tgt='ITRF2020', epoch=2024.0)
"""
if not isinstance(src, str) or not src:
raise ValueError(
f"itrf_transform(): src must be a non-empty string, got {src!r}"
)
if not isinstance(tgt, str) or not tgt:
raise ValueError(
f"itrf_transform(): tgt must be a non-empty string, got {tgt!r}"
)

try:
epoch_float = float(epoch)
except (TypeError, ValueError):
raise ValueError(
f"itrf_transform(): epoch must be a finite number, got {epoch!r}"
)
if not np.isfinite(epoch_float):
raise ValueError(
f"itrf_transform(): epoch must be a finite number, got {epoch!r}"
)

raw_params, is_reverse = _find_transform(src, tgt)
if raw_params is None:
raise ValueError(
Expand Down
13 changes: 13 additions & 0 deletions xrspatial/reproject/_vertical.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,19 @@ def geoid_height(lon, lat, model='EGM96'):
lon_arr = np.atleast_1d(np.asarray(lon, dtype=np.float64)).ravel()
lat_arr = np.atleast_1d(np.asarray(lat, dtype=np.float64)).ravel()

if not np.isfinite(lon_arr).all():
raise ValueError(
"geoid_height(): lon contains non-finite values (NaN or Inf)."
)
if not np.isfinite(lat_arr).all():
raise ValueError(
"geoid_height(): lat contains non-finite values (NaN or Inf)."
)
if not ((lat_arr >= -90.0) & (lat_arr <= 90.0)).all():
raise ValueError(
"geoid_height(): lat must be in [-90, 90]."
)

out = np.empty(lon_arr.shape[0], dtype=np.float64)
_interp_geoid_batch(lon_arr, lat_arr, out, data, left, top,
res_x, res_y, h, w)
Expand Down
77 changes: 77 additions & 0 deletions xrspatial/tests/test_reproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -1517,3 +1517,80 @@ def test_numpy_chunk_source_window_guard(self):
)
result = reproject(raster, target_crs='EPSG:3857')
assert result.shape[0] > 0 and result.shape[1] > 0


# =====================================================================
# Issue #1435: NaN/Inf rejection in scalar inputs
# =====================================================================

class TestItrfFiniteness:
@pytest.mark.parametrize("epoch", [float('nan'), float('inf'), float('-inf')])
def test_itrf_rejects_non_finite_epoch(self, epoch):
from xrspatial.reproject import itrf_transform
with pytest.raises(ValueError, match="epoch"):
itrf_transform(0.0, 0.0, 0.0,
src='ITRF2014', tgt='ITRF2020', epoch=epoch)

def test_itrf_rejects_empty_src(self):
from xrspatial.reproject import itrf_transform
with pytest.raises(ValueError, match="src"):
itrf_transform(0.0, 0.0, 0.0,
src='', tgt='ITRF2020', epoch=2024.0)

def test_itrf_rejects_empty_tgt(self):
from xrspatial.reproject import itrf_transform
with pytest.raises(ValueError, match="tgt"):
itrf_transform(0.0, 0.0, 0.0,
src='ITRF2014', tgt='', epoch=2024.0)


class TestGeoidFiniteness:
@pytest.mark.parametrize("lon", [float('nan'), float('inf')])
def test_geoid_rejects_non_finite_lon(self, lon):
from xrspatial.reproject import geoid_height
with pytest.raises(ValueError, match="lon"):
geoid_height(lon, 0.0)

@pytest.mark.parametrize("lat", [float('nan'), float('inf')])
def test_geoid_rejects_non_finite_lat(self, lat):
from xrspatial.reproject import geoid_height
with pytest.raises(ValueError, match="lat"):
geoid_height(0.0, lat)

@pytest.mark.parametrize("lat", [-91.0, 91.0])
def test_geoid_rejects_out_of_range_lat(self, lat):
from xrspatial.reproject import geoid_height
with pytest.raises(ValueError, match=r"\[-90, 90\]"):
geoid_height(0.0, lat)

def test_geoid_rejects_array_with_nan(self):
from xrspatial.reproject import geoid_height
lon = np.array([0.0, float('nan'), 10.0])
lat = np.array([0.0, 0.0, 0.0])
with pytest.raises(ValueError, match="lon"):
geoid_height(lon, lat)


class TestNodataFiniteness:
def test_detect_nodata_rejects_inf(self):
from xrspatial.reproject._crs_utils import _detect_nodata
r = xr.DataArray(np.zeros((4, 4)), dims=('y', 'x'))
with pytest.raises(ValueError, match="nodata"):
_detect_nodata(r, nodata=float('inf'))

def test_detect_nodata_rejects_neg_inf(self):
from xrspatial.reproject._crs_utils import _detect_nodata
r = xr.DataArray(np.zeros((4, 4)), dims=('y', 'x'))
with pytest.raises(ValueError, match="nodata"):
_detect_nodata(r, nodata=float('-inf'))

def test_detect_nodata_accepts_nan(self):
from xrspatial.reproject._crs_utils import _detect_nodata
r = xr.DataArray(np.zeros((4, 4)), dims=('y', 'x'))
nd = _detect_nodata(r, nodata=float('nan'))
assert np.isnan(nd)

def test_detect_nodata_accepts_finite(self):
from xrspatial.reproject._crs_utils import _detect_nodata
r = xr.DataArray(np.zeros((4, 4)), dims=('y', 'x'))
assert _detect_nodata(r, nodata=-9999) == -9999.0
Loading