Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Attention: The newest changes should be on top -->

### Fixed

- BUG: fix NaN in ND linear interpolation outside convex hull [#926](https://github.com/RocketPy-Team/RocketPy/issues/926)
- BUG: Add wraparound logic for wind direction in environment plots [#939](https://github.com/RocketPy-Team/RocketPy/pull/939)

## [v1.12.1] - 2026-04-03
Expand Down
12 changes: 9 additions & 3 deletions rocketpy/mathutils/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
RBFInterpolator,
RegularGridInterpolator,
)
from scipy.spatial import Delaunay # pylint: disable=no-name-in-module

from rocketpy.plots.plot_helpers import show_or_save_plot
from rocketpy.tools import deprecated, from_hex_decode, to_hex_encode
Expand Down Expand Up @@ -519,7 +520,9 @@ def linear_interpolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: di
return (x - x_left) * (dy / dx) + y_left

else:
interpolator = LinearNDInterpolator(self._domain, self._image)
tri = Delaunay(self._domain)
interpolator = LinearNDInterpolator(tri, self._image)
self._nd_triangulation = tri

def linear_interpolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: disable=unused-argument
return interpolator(x)
Expand Down Expand Up @@ -827,8 +830,11 @@ def __get_value_opt_nd(self, *args):
min_domain = self._domain.T.min(axis=1)
max_domain = self._domain.T.max(axis=1)

lower, upper = args < min_domain, args > max_domain
extrap = np.logical_or(lower.any(axis=1), upper.any(axis=1))
if self.__interpolation__ == "linear" and hasattr(self, "_nd_triangulation"):
extrap = self._nd_triangulation.find_simplex(args) < 0
else:
lower, upper = args < min_domain, args > max_domain
extrap = np.logical_or(lower.any(axis=1), upper.any(axis=1))

if extrap.any():
result[extrap] = self._extrapolation_func(
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/mathutils/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -1505,3 +1505,27 @@ def test_regular_grid_invalid_source_raises(bad_source, match):
outputs=["z"],
interpolation="regular_grid",
)


def test_2d_linear_interpolation_no_nan_outside_convex_hull():
"""Test that querying a point inside the bounding box but outside the
convex hull does not silently return NaN.

Regression test for https://github.com/RocketPy-Team/RocketPy/issues/926.
The point (0.3, 0.3) lies within the axis-aligned bounding box of the
data but outside the convex hull, so LinearNDInterpolator would return
NaN without proper hull detection.
"""
data = [
[0.0, 0.0, 0.000],
[0.0, 0.1, 0.100],
[0.0, 0.4, 0.400],
[0.3, 0.2, 0.150],
]
func = Function(data, interpolation="linear")
result = func(0.3, 0.3)

assert not np.isnan(result), (
"f(0.3, 0.3) returned NaN. Point is outside the convex hull and "
"should be routed to extrapolation, not silently return NaN."
)
Loading