surface_distance, surface_allocation and surface_direction declare target_values: list = []. Every other function in the same family declares target_values: list = None and normalizes it to [] in the body:
| function |
file |
default |
proximity |
xrspatial/proximity.py:1607 |
None |
allocation |
xrspatial/proximity.py:1767 |
None |
direction |
xrspatial/proximity.py:1928 |
None |
cost_distance |
xrspatial/cost_distance.py:1219 |
None |
surface_distance |
xrspatial/surface_distance.py:1424 |
[] |
surface_allocation |
xrspatial/surface_distance.py:1485 |
[] |
surface_direction |
xrspatial/surface_distance.py:1527 |
[] |
Two consequences.
The first is that passing target_values=None explodes. The proximity trio and cost_distance accept it because they do if target_values is None: target_values = []. surface_distance has no such guard, so None reaches np.asarray(None, dtype=np.float64), which produces a 0-d array, which is then handed to the _seed_sources numba kernel and dies in type inference. The traceback runs about 40 lines about getitem(array(float64, 0d, C), int64) and never mentions target_values.
That is not a contrived call. Wrapper code routinely threads target_values through from its own optional argument, which is None when the caller did not set it. The pattern works against four functions in this family and fails against three.
The second is that [] as a default is a mutable default argument. Nothing mutates it today, since _compute immediately does np.asarray, so there is no live aliasing bug. It is still the anti-pattern the sibling functions already avoid.
Reproduction
import numpy as np
import xarray as xr
from xrspatial import (proximity, allocation, direction,
surface_distance, surface_allocation, surface_direction)
from xrspatial.cost_distance import cost_distance
H = W = 8
src = np.zeros((H, W)); src[0, 0] = 1.0
elev = np.zeros((H, W))
coords = {'y': np.arange(H)[::-1].astype(float), 'x': np.arange(W).astype(float)}
raster = xr.DataArray(src, dims=['y', 'x'], coords=coords)
elevation = xr.DataArray(elev, dims=['y', 'x'], coords=coords)
for name, fn, args in [
("proximity", proximity, (raster,)),
("allocation", allocation, (raster,)),
("direction", direction, (raster,)),
("cost_distance", cost_distance, (raster, elevation)),
("surface_distance", surface_distance, (raster, elevation)),
("surface_allocation", surface_allocation, (raster, elevation)),
("surface_direction", surface_direction, (raster, elevation)),
]:
try:
fn(*args, target_values=None)
print(f"{name}(target_values=None) -> OK")
except Exception as e:
print(f"{name}(target_values=None) -> {type(e).__name__}")
Observed on this host (Python 3.14.2, numba 0.65.0, numpy 2.3.2):
proximity(target_values=None) -> OK
allocation(target_values=None) -> OK
direction(target_values=None) -> OK
cost_distance(target_values=None) -> OK
surface_distance(target_values=None) -> TypingError
surface_allocation(target_values=None) -> TypingError
surface_direction(target_values=None) -> TypingError
The TypingError body:
No implementation of function Function(<built-in function getitem>) found for signature:
>>> getitem(array(float64, 0d, C), int64)
...
File "xrspatial/surface_distance.py", line 193:
for k in range(n_values):
if val == target_values[k]:
^
Proposed fix
Change the three signatures to target_values: list = None and add the same if target_values is None: target_values = [] normalization the siblings use. This is not a breaking change: [] and None both mean "treat every non-zero finite pixel as a source", callers passing an explicit list are unaffected, and callers passing None go from a numba traceback to working code. No deprecation shim is needed because no parameter is renamed and no accepted input changes meaning.
Notes
Found by /sweep-api-consistency on surface_distance (category 1 and 4, MEDIUM).
balanced_allocation (xrspatial/balanced_allocation.py) carries the same target_values: list = [] default. Out of scope for this issue, but worth a separate look to see whether it fails the same way.
surface_distance,surface_allocationandsurface_directiondeclaretarget_values: list = []. Every other function in the same family declarestarget_values: list = Noneand normalizes it to[]in the body:proximityxrspatial/proximity.py:1607Noneallocationxrspatial/proximity.py:1767Nonedirectionxrspatial/proximity.py:1928Nonecost_distancexrspatial/cost_distance.py:1219Nonesurface_distancexrspatial/surface_distance.py:1424[]surface_allocationxrspatial/surface_distance.py:1485[]surface_directionxrspatial/surface_distance.py:1527[]Two consequences.
The first is that passing
target_values=Noneexplodes. The proximity trio andcost_distanceaccept it because they doif target_values is None: target_values = [].surface_distancehas no such guard, soNonereachesnp.asarray(None, dtype=np.float64), which produces a 0-d array, which is then handed to the_seed_sourcesnumba kernel and dies in type inference. The traceback runs about 40 lines aboutgetitem(array(float64, 0d, C), int64)and never mentionstarget_values.That is not a contrived call. Wrapper code routinely threads
target_valuesthrough from its own optional argument, which isNonewhen the caller did not set it. The pattern works against four functions in this family and fails against three.The second is that
[]as a default is a mutable default argument. Nothing mutates it today, since_computeimmediately doesnp.asarray, so there is no live aliasing bug. It is still the anti-pattern the sibling functions already avoid.Reproduction
Observed on this host (Python 3.14.2, numba 0.65.0, numpy 2.3.2):
The
TypingErrorbody:Proposed fix
Change the three signatures to
target_values: list = Noneand add the sameif target_values is None: target_values = []normalization the siblings use. This is not a breaking change:[]andNoneboth mean "treat every non-zero finite pixel as a source", callers passing an explicit list are unaffected, and callers passingNonego from a numba traceback to working code. No deprecation shim is needed because no parameter is renamed and no accepted input changes meaning.Notes
Found by
/sweep-api-consistencyonsurface_distance(category 1 and 4, MEDIUM).balanced_allocation(xrspatial/balanced_allocation.py) carries the sametarget_values: list = []default. Out of scope for this issue, but worth a separate look to see whether it fails the same way.