Describe the bug
_get_pixel_id() in xrspatial/pathfinding.py converts a (y, x) point to a pixel index with
py = int(abs(point[0] - y_coords[0]) / cellsize_y)
px = int(abs(point[1] - x_coords[0]) / cellsize_x)
Two problems come out of this:
-
abs() folds out-of-bounds points back inside the raster. A point past the edge on the coords[0] side gets a positive offset and maps to a mirrored interior pixel. a_star_search then runs from the wrong start pixel instead of raising the "start location outside the surface graph" error it raises for points past the opposite edge. multi_stop_search's waypoint bounds check has the same blind spot.
-
int() truncates instead of rounding to the nearest cell center. Any point in the upper half of a cell is assigned to the neighboring lower-index cell (a half-cell bias), and a point that should land exactly on a cell center can flip to the previous cell through float noise: on a grid with 0.1 resolution, x=0.3 maps to pixel 2 because the pixel-3 center is 0.30000000000000004.
Reproduction (run on this host):
import numpy as np
import xarray as xr
from xrspatial import a_star_search
from xrspatial.pathfinding import _get_pixel_id
h = w = 5
agg = xr.DataArray(np.ones((h, w)), dims=['y', 'x'], attrs={'res': (1.0, 1.0)})
agg['y'] = np.linspace(h - 1, 0, h) # [4, 3, 2, 1, 0]
agg['x'] = np.linspace(0, w - 1, w)
# 1) start y=5.0 is OUTSIDE the raster (top edge is y=4.0)
path = a_star_search(agg, (5.0, 0.0), (0.0, 4.0))
print(np.argwhere(path.values == 0)) # -> [[1 0]] path starts at row 1, no error
# 2) truncation
agg2 = xr.DataArray(np.ones((2, 10)), dims=['y', 'x'])
agg2['y'] = [0.1, 0.0]
agg2['x'] = np.arange(10) * 0.1
print(_get_pixel_id((0.0, 0.3), agg2, 'x', 'y')) # -> (0, 2), pixel-3 center is 0.30000000000000004
print(_get_pixel_id((0.0, 2.6), agg, 'x', 'y')) # -> (?, 2) on a unit grid, nearest center is 3
Observed output:
Expected behavior
- A point outside the raster bounds raises
ValueError ("start location outside the surface graph") regardless of which side it falls on.
- A point maps to the cell whose center is nearest, so
x=0.3 on a 0.1-resolution grid resolves to pixel 3 and x=2.6 on a unit grid resolves to pixel 3.
The fix is to compute a signed offset using the coordinate direction and round to the nearest index; negative indices then fail the existing _is_inside check and raise as intended.
Additional context
Found by the accuracy sweep (Cat 1/Cat 3). The folding half is rated HIGH (silent wrong path from a mis-mapped start/goal), the truncation half MEDIUM. Both a_star_search and multi_stop_search route through _get_pixel_id. Existing tests only use points that sit exactly on cell centers, so they pass under either convention.
Describe the bug
_get_pixel_id()inxrspatial/pathfinding.pyconverts a(y, x)point to a pixel index withTwo problems come out of this:
abs()folds out-of-bounds points back inside the raster. A point past the edge on thecoords[0]side gets a positive offset and maps to a mirrored interior pixel.a_star_searchthen runs from the wrong start pixel instead of raising the "start location outside the surface graph" error it raises for points past the opposite edge.multi_stop_search's waypoint bounds check has the same blind spot.int()truncates instead of rounding to the nearest cell center. Any point in the upper half of a cell is assigned to the neighboring lower-index cell (a half-cell bias), and a point that should land exactly on a cell center can flip to the previous cell through float noise: on a grid with 0.1 resolution,x=0.3maps to pixel 2 because the pixel-3 center is0.30000000000000004.Reproduction (run on this host):
Observed output:
Expected behavior
ValueError("start location outside the surface graph") regardless of which side it falls on.x=0.3on a 0.1-resolution grid resolves to pixel 3 andx=2.6on a unit grid resolves to pixel 3.The fix is to compute a signed offset using the coordinate direction and round to the nearest index; negative indices then fail the existing
_is_insidecheck and raise as intended.Additional context
Found by the accuracy sweep (Cat 1/Cat 3). The folding half is rated HIGH (silent wrong path from a mis-mapped start/goal), the truncation half MEDIUM. Both
a_star_searchandmulti_stop_searchroute through_get_pixel_id. Existing tests only use points that sit exactly on cell centers, so they pass under either convention.