Found in a retrospective adversarial review of #430 after merge.
The inconsistency
KDTree.rbf_interpolator_local rejects nnn=1 with order=1, because one neighbour cannot determine an affine tail:
kdt.rbf_interpolator_local(tgt, f, 1, 2, False, order=1)
# ValueError: order=1 needs at least dim + 2 neighbours ...
KDTree.interpolation_matrix does not:
T = kdt.interpolation_matrix(tgt, nnn=1, order=1)
# no error; nnz/row = 1 -- a nearest-neighbour operator
Measured on development at 4d043aeb.
Why it matters
The caller asked for a linear-exact operator and silently got a nearest-neighbour one, which reproduces constants only. Nothing in the returned object records the downgrade. The whole point of the order=1 path is a guarantee; handing back an operator that does not have it, without saying so, is the failure mode the rest of that PR was written to avoid.
It also contradicts the PR's own claim that the value and operator paths are built on one core and "cannot drift apart". They share _local_stencil, but _local_stencil early-returns for nnn == 1 before reaching the order handling, and only rbf_interpolator_local_from_kdtree carries the guard — interpolation_matrix does not.
Fix
Move the nnn == 1 and order == 1 guard into _local_stencil, so both callers inherit it. That is where the rest of the shared behaviour already lives.
Also worth tightening while there
interpolation_matrix with nnn > self.n raises RuntimeError: Error in rbf_interpolator_local_from_kdtree - a nearest neighbour wasn't found, naming a function the caller never invoked. Correct behaviour, confusing message.
- The degenerate-fallback
warnings.warn(..., stacklevel=3) is tuned for the value path's call depth; from interpolation_matrix the depth differs by one, so attribution is off by a frame.
Test gap
test_0102_kdtree_linear_rbf.py::test_interpolation_matrix_agrees_with_the_value_path parametrises order but not nnn, and never exercises nnn=1. A guard test on both entry points would have caught this.
Found in a retrospective adversarial review of #430 after merge.
The inconsistency
KDTree.rbf_interpolator_localrejectsnnn=1withorder=1, because one neighbour cannot determine an affine tail:KDTree.interpolation_matrixdoes not:Measured on
developmentat4d043aeb.Why it matters
The caller asked for a linear-exact operator and silently got a nearest-neighbour one, which reproduces constants only. Nothing in the returned object records the downgrade. The whole point of the
order=1path is a guarantee; handing back an operator that does not have it, without saying so, is the failure mode the rest of that PR was written to avoid.It also contradicts the PR's own claim that the value and operator paths are built on one core and "cannot drift apart". They share
_local_stencil, but_local_stencilearly-returns fornnn == 1before reaching theorderhandling, and onlyrbf_interpolator_local_from_kdtreecarries the guard —interpolation_matrixdoes not.Fix
Move the
nnn == 1 and order == 1guard into_local_stencil, so both callers inherit it. That is where the rest of the shared behaviour already lives.Also worth tightening while there
interpolation_matrixwithnnn > self.nraisesRuntimeError: Error in rbf_interpolator_local_from_kdtree - a nearest neighbour wasn't found, naming a function the caller never invoked. Correct behaviour, confusing message.warnings.warn(..., stacklevel=3)is tuned for the value path's call depth; frominterpolation_matrixthe depth differs by one, so attribution is off by a frame.Test gap
test_0102_kdtree_linear_rbf.py::test_interpolation_matrix_agrees_with_the_value_pathparametrisesorderbut notnnn, and never exercisesnnn=1. A guard test on both entry points would have caught this.