From 181eec670a0f93933fe1d920f93934845cffff03 Mon Sep 17 00:00:00 2001 From: TIM ANDERSON Date: Sat, 26 Sep 2026 18:23:39 +1000 Subject: [PATCH] fix: reject incomplete NTv2 bicubic stencils at subgrid edges --- docs/features/ntv2reader.rst | 14 +++ geodepy/ntv2reader.py | 15 ++- geodepy/tests/test_ntv2reader.py | 159 +++++++++++++++++++++++++++++++ geodepy/transform.py | 4 +- 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 geodepy/tests/test_ntv2reader.py diff --git a/docs/features/ntv2reader.rst b/docs/features/ntv2reader.rst index 5caae8c..45e9f5f 100644 --- a/docs/features/ntv2reader.rst +++ b/docs/features/ntv2reader.rst @@ -6,6 +6,20 @@ NTV2 Reader Tihs module provides functionality to read and utilize NTV2 grid files for coordinate transformations. It has been adapted from Jaimie Dodd's ntv2reader.py +Interpolation near subgrid boundaries +------------------------------------ + +Bicubic interpolation requires a 4-by-4 neighbourhood of nodes entirely within +the selected subgrid. It raises ``ValueError`` where this neighbourhood is +unavailable, including cells along the subgrid edges and subgrids with fewer +than four rows or columns. This prevents reads from neighbouring rows, +subgrid headers, or beyond the file. + +For these locations, explicitly select ``method="bilinear"`` when calling +``interpolate_ntv2`` or ``geodepy.transform.ntv2_2d``. The interpolation method +is not changed automatically. Subgrid selection and the existing exclusion +of coordinates on the northern and western extents are unchanged. + Classes ------- diff --git a/geodepy/ntv2reader.py b/geodepy/ntv2reader.py index 2ae9bf7..6aab242 100644 --- a/geodepy/ntv2reader.py +++ b/geodepy/ntv2reader.py @@ -184,9 +184,20 @@ def ntv2_bicubic(self, lat, lon, num_cols, row, col, f, start_byte): :param start_byte: start index of subgrid :return: Four field tuple of interpolation results at point of interest. + :raises ValueError: If the 4x4 interpolation stencil extends outside + this subgrid. Use bilinear interpolation instead. """ + # Check rows and columns separately: a valid file offset can still + # wrap into another row or read a neighbouring subgrid's header/data. + num_rows = self.gs_count // num_cols + if not (1 <= row < num_rows - 2 and 1 <= col < num_cols - 2): + raise ValueError( + "Bicubic interpolation requires a complete 4x4 stencil within " + "the subgrid; use method='bilinear' near subgrid boundaries" + ) + # | | | | # --o-----o-----o-----o-- # |11 |12 |13 |14 @@ -666,13 +677,15 @@ def read_ntv2_file(ntv2_gsb_file): def interpolate_ntv2(grid_object, lat, lon, method="bicubic"): """ Function to interpolate Ntv2Grid objects - + :param grid_object: Ntv2Grid object :param lat: latitude (decimal degrees) :param lon: longitude (decimal degrees) :param method: interpolation strategy, bicubic or bilinear :return: Tuple of four ntv2 fields + :raises ValueError: If bicubic interpolation lacks a complete 4x4 stencil + in the selected subgrid. Use method='bilinear' instead. """ interpolation_methods = {"bicubic", "bilinear"} diff --git a/geodepy/tests/test_ntv2reader.py b/geodepy/tests/test_ntv2reader.py new file mode 100644 index 0000000..2f4b777 --- /dev/null +++ b/geodepy/tests/test_ntv2reader.py @@ -0,0 +1,159 @@ +"""NTv2 interpolation regression tests using small, generated binary grids.""" + +import struct +import tempfile +import unittest +from pathlib import Path +from unittest.mock import Mock + +from geodepy.ntv2reader import interpolate_ntv2, read_ntv2_file +from geodepy.transform import ntv2_2d + + +def write_grid(path, sizes=(6,), nonlinear=False): + """Write valid little-endian NTv2 headers and independent planar fields.""" + + def record(name, value): + if isinstance(value, str): + data = value.encode("ascii").ljust(8, b" ") + elif isinstance(value, int): + data = struct.pack("