|
| 1 | +"""Regression test for issue #1720. |
| 2 | +
|
| 3 | +``_coords_to_transform`` previously read ``x[1] - x[0]`` and |
| 4 | +``y[1] - y[0]`` as the pixel sizes without checking that the rest of |
| 5 | +the spacing matched. GeoTIFF supports only an affine transform, so |
| 6 | +non-uniform coords cannot be expressed faithfully; the writer would |
| 7 | +silently use the first-step spacing and produce a wrong transform. |
| 8 | +
|
| 9 | +The fix validates ``np.diff(x)`` and ``np.diff(y)`` against a relative |
| 10 | +tolerance of ``1e-6`` of the median step and raises ``ValueError`` if |
| 11 | +spacing is non-uniform. |
| 12 | +""" |
| 13 | +from __future__ import annotations |
| 14 | + |
| 15 | +import numpy as np |
| 16 | +import pytest |
| 17 | +import xarray as xr |
| 18 | + |
| 19 | +from xrspatial.geotiff import _coords_to_transform, to_geotiff |
| 20 | + |
| 21 | + |
| 22 | +def _make_da(x_coords, y_coords): |
| 23 | + arr = np.zeros((len(y_coords), len(x_coords)), dtype=np.uint8) |
| 24 | + return xr.DataArray( |
| 25 | + arr, |
| 26 | + dims=['y', 'x'], |
| 27 | + coords={'y': np.asarray(y_coords), 'x': np.asarray(x_coords)}, |
| 28 | + ) |
| 29 | + |
| 30 | + |
| 31 | +def test_uniform_coords_ok(): |
| 32 | + """Uniform coords write successfully (no regression).""" |
| 33 | + da = _make_da(np.linspace(500.0, 700.0, 20), np.linspace(100.0, 200.0, 10)) |
| 34 | + gt = _coords_to_transform(da) |
| 35 | + assert gt is not None |
| 36 | + np.testing.assert_allclose(gt.pixel_width, (700.0 - 500.0) / 19) |
| 37 | + np.testing.assert_allclose(gt.pixel_height, (200.0 - 100.0) / 9) |
| 38 | + |
| 39 | + |
| 40 | +def test_uniform_coords_roundtrip_to_geotiff_1720(tmp_path): |
| 41 | + """End-to-end write succeeds on uniform coords.""" |
| 42 | + da = _make_da(np.linspace(500.0, 700.0, 20), np.linspace(100.0, 200.0, 10)) |
| 43 | + p = str(tmp_path / 'uniform_1720.tif') |
| 44 | + to_geotiff(da, p) |
| 45 | + |
| 46 | + |
| 47 | +def test_non_uniform_x_raises_1720(): |
| 48 | + """Non-uniform x coords raise ValueError naming x.""" |
| 49 | + # Mostly-uniform x with one stretched gap to expose the bug. |
| 50 | + x = np.array([0.0, 1.0, 2.0, 3.0, 5.0]) |
| 51 | + y = np.linspace(0.0, 10.0, 11) |
| 52 | + da = _make_da(x, y) |
| 53 | + with pytest.raises(ValueError, match=r"\bx coords are not uniformly spaced"): |
| 54 | + _coords_to_transform(da) |
| 55 | + |
| 56 | + |
| 57 | +def test_non_uniform_y_raises_1720(): |
| 58 | + """Non-uniform y coords raise ValueError naming y.""" |
| 59 | + x = np.linspace(0.0, 10.0, 11) |
| 60 | + y = np.array([0.0, 1.0, 2.0, 3.0, 5.0]) |
| 61 | + da = _make_da(x, y) |
| 62 | + with pytest.raises(ValueError, match=r"\by coords are not uniformly spaced"): |
| 63 | + _coords_to_transform(da) |
| 64 | + |
| 65 | + |
| 66 | +def test_jitter_within_tolerance_ok_1720(): |
| 67 | + """Float jitter within 1e-6 relative tolerance writes successfully.""" |
| 68 | + x = np.linspace(0.0, 100.0, 11) |
| 69 | + # Add jitter at ~1e-8 relative scale — well below the 1e-6 threshold. |
| 70 | + rng = np.random.default_rng(0) |
| 71 | + x = x + rng.uniform(-1e-7, 1e-7, size=x.shape) |
| 72 | + y = np.linspace(0.0, 50.0, 6) |
| 73 | + da = _make_da(x, y) |
| 74 | + gt = _coords_to_transform(da) |
| 75 | + assert gt is not None |
| 76 | + |
| 77 | + |
| 78 | +def test_jitter_just_above_tolerance_raises_1720(): |
| 79 | + """Jitter just above the 1e-6 relative tolerance raises ValueError.""" |
| 80 | + # Step size is 10; one diff is 10 + 1e-4 -> relative deviation ~1e-5, |
| 81 | + # which exceeds the 1e-6 threshold. |
| 82 | + x = np.array([0.0, 10.0, 20.0001, 30.0001, 40.0001]) |
| 83 | + y = np.linspace(0.0, 50.0, 6) |
| 84 | + da = _make_da(x, y) |
| 85 | + with pytest.raises(ValueError, match=r"max relative deviation"): |
| 86 | + _coords_to_transform(da) |
| 87 | + |
| 88 | + |
| 89 | +def test_two_sample_coords_ok_1720(): |
| 90 | + """Two-sample coords (one diff) trivially pass the regularity check.""" |
| 91 | + x = np.array([0.0, 10.0]) |
| 92 | + y = np.array([0.0, 5.0]) |
| 93 | + da = _make_da(x, y) |
| 94 | + gt = _coords_to_transform(da) |
| 95 | + assert gt is not None |
| 96 | + np.testing.assert_allclose(gt.pixel_width, 10.0) |
| 97 | + np.testing.assert_allclose(gt.pixel_height, 5.0) |
| 98 | + |
| 99 | + |
| 100 | +def test_constant_coords_raises_1720(): |
| 101 | + """Constant coords (step == 0) raise ValueError.""" |
| 102 | + x = np.array([0.0, 0.0, 0.0, 0.0]) |
| 103 | + y = np.linspace(0.0, 10.0, 4) |
| 104 | + da = _make_da(x, y) |
| 105 | + with pytest.raises(ValueError, match=r"x coords are constant"): |
| 106 | + _coords_to_transform(da) |
0 commit comments