空间模块的热路径上有四处「不报错但结果是错的」,都会把错误伪装成合法数据。按危害排序:
1. 整数栅格的掩膜从未生效
abses/utils/func.py 的 set_null_values 对整数数组重绑定局部变量:
if arr.dtype.kind in {"f", "i"}:
if arr.dtype.kind == "i":
arr = arr.astype(float, copy=True) # ← 之后的写入落在副本上
null_value = np.nan
...
arr[mask] = null_value
return arr
而 abses/space/patch.py 的 _add_attribute 丢弃返回值:
if apply_mask:
set_null_values(data, ~self.mask) # ← 返回值无人接收
结果:float / str 数组按预期被掩膜,int 数组完全没有被掩膜,掩膜区的原始整数值被当作有效数据写进格子。同一个函数对不同 dtype 一会儿原地改、一会儿返副本,是这个 bug 的根。
建议:set_null_values 统一为「返回新数组」的纯函数语义,调用点改为 data = set_null_values(data, ~self.mask)。
2. apply_raster 遇到不支持的输入静默返回 None
abses/space/patch.py 的类型分派没有 else:
if isinstance(data, np.ndarray):
...
elif isinstance(data, xr.DataArray):
...
elif isinstance(data, xr.Dataset):
...
# 没有 else
module.apply_raster([1, 2, 3], attr_name="x") 返回 None、不加属性、不报错。
建议:补 else: raise TypeError(...)。
3. mask setter 不清 cells_lst 的缓存
@functools.cached_property
def cells_lst(self): return ActorsList(self.model, self.array_cells[self.mask])
@mask.setter
def mask(self, array): self._mask = array.astype(bool) # ← 没有失效缓存
一旦 cells_lst / agents / random / select({...}) 中任何一个先被读过,之后再赋值 module.mask 就被静默忽略;而 xda 读的是实时 mask,于是同一个对象的两个视图会给出互相矛盾的结果。
建议:setter 里 self.__dict__.pop("cells_lst", None)。
4. apply_agents 把所有用户异常转成 NaN
abses/space/patch.py 的 apply_agents 里有四个 except Exception,其中 _to_float 连日志都不打:
def _to_float(val):
try:
return float(val)
except Exception:
return float("nan") # 静默
因为跑在 np.vectorize 里逐格子执行,一个写坏的 aggregator 会:
- 刷出与格子数同量级的 warning(大栅格可达 10^6 行);
- 返回一个看起来完全合法的全 NaN 数组。
建议:改为累计失败计数、只发一条聚合 warning(附第一条 traceback),并让非用户错误(MemoryError、拼写导致的 AttributeError 等)正常向上抛。
验收标准
空间模块的热路径上有四处「不报错但结果是错的」,都会把错误伪装成合法数据。按危害排序:
1. 整数栅格的掩膜从未生效
abses/utils/func.py的set_null_values对整数数组重绑定局部变量:而
abses/space/patch.py的_add_attribute丢弃返回值:结果:float / str 数组按预期被掩膜,int 数组完全没有被掩膜,掩膜区的原始整数值被当作有效数据写进格子。同一个函数对不同 dtype 一会儿原地改、一会儿返副本,是这个 bug 的根。
建议:
set_null_values统一为「返回新数组」的纯函数语义,调用点改为data = set_null_values(data, ~self.mask)。2.
apply_raster遇到不支持的输入静默返回 Noneabses/space/patch.py的类型分派没有else:module.apply_raster([1, 2, 3], attr_name="x")返回None、不加属性、不报错。建议:补
else: raise TypeError(...)。3.
masksetter 不清cells_lst的缓存一旦
cells_lst/agents/random/select({...})中任何一个先被读过,之后再赋值module.mask就被静默忽略;而xda读的是实时mask,于是同一个对象的两个视图会给出互相矛盾的结果。建议:setter 里
self.__dict__.pop("cells_lst", None)。4.
apply_agents把所有用户异常转成 NaNabses/space/patch.py的apply_agents里有四个except Exception,其中_to_float连日志都不打:因为跑在
np.vectorize里逐格子执行,一个写坏的aggregator会:建议:改为累计失败计数、只发一条聚合 warning(附第一条 traceback),并让非用户错误(
MemoryError、拼写导致的AttributeError等)正常向上抛。验收标准
apply_raster(int_array, apply_mask=True)后,掩膜区的格子属性为 NaN。apply_raster传入不支持的类型时抛TypeError。cells_lst之后再改mask,cells_lst与xda保持一致。apply_agents的 aggregator 抛异常时,warning 条数不随栅格大小增长。