4040 "regular_grid" : 6 ,
4141}
4242EXTRAPOLATION_TYPES = {"zero" : 0 , "natural" : 1 , "constant" : 2 }
43+ # Maps a requested interpolation name onto a scipy ``RegularGridInterpolator``
44+ # ``method`` for gridded (N-D Cartesian) data. The 1-D-only names ``spline`` and
45+ # ``akima`` fall back to their closest grid analogs (``cubic`` and the
46+ # shape-preserving ``pchip``); anything unrecognized defaults to ``linear``.
47+ REGULAR_GRID_METHODS = {
48+ "linear" : "linear" ,
49+ "nearest" : "nearest" ,
50+ "slinear" : "slinear" ,
51+ "cubic" : "cubic" ,
52+ "quintic" : "quintic" ,
53+ "pchip" : "pchip" ,
54+ "spline" : "cubic" ,
55+ "akima" : "pchip" ,
56+ "polynomial" : "cubic" ,
57+ }
58+ # Minimum points per axis required by each ``RegularGridInterpolator`` method.
59+ # A grid with fewer samples on any axis cannot use the higher-order methods, so
60+ # the caller falls back to linear rather than letting SciPy raise mid-build.
61+ REGULAR_GRID_MIN_POINTS = {
62+ "nearest" : 1 ,
63+ "linear" : 2 ,
64+ "slinear" : 2 ,
65+ "pchip" : 2 ,
66+ "cubic" : 4 ,
67+ "quintic" : 6 ,
68+ }
4369
4470
4571class SourceType (Enum ):
@@ -157,7 +183,12 @@ def __init__(
157183
158184 @classmethod
159185 def from_regular_grid_csv (
160- cls , csv_source , variable_names , coeff_name , extrapolation
186+ cls ,
187+ csv_source ,
188+ variable_names ,
189+ coeff_name ,
190+ extrapolation ,
191+ interpolation = "linear" ,
161192 ):
162193 """Create a regular-grid Function from CSV samples when possible.
163194
@@ -171,6 +202,14 @@ def from_regular_grid_csv(
171202 Name of the output coefficient.
172203 extrapolation : str
173204 Extrapolation method passed to the Function constructor.
205+ interpolation : str, optional
206+ Requested interpolation. Mapped onto a
207+ :class:`scipy.interpolate.RegularGridInterpolator` ``method`` via
208+ :data:`REGULAR_GRID_METHODS` (e.g. ``"spline"`` -> ``"cubic"``,
209+ ``"akima"`` -> ``"pchip"``); unrecognized names fall back to
210+ ``"linear"``. Smooth methods require enough points per axis
211+ (``"cubic"`` needs at least 4), otherwise SciPy raises. Default
212+ ``"linear"``.
174213
175214 Returns
176215 -------
@@ -215,13 +254,33 @@ def from_regular_grid_csv(
215254 return None
216255
217256 grid_data = sorted_values .reshape (tuple (axis .size for axis in axes ))
218- return cls (
257+ grid_function = cls (
219258 (axes , grid_data ),
220259 inputs = variable_names ,
221260 outputs = [coeff_name ],
222261 interpolation = "regular_grid" ,
223262 extrapolation = extrapolation ,
224263 )
264+ # Honor the requested interpolation on the grid by rebuilding the
265+ # interpolator/extrapolator with the mapped scipy ``method``. The
266+ # constructor above always builds the default ("linear"); only rebuild
267+ # when a different method was asked for.
268+ grid_method = REGULAR_GRID_METHODS .get (interpolation , "linear" )
269+ smallest_axis = min (axis .size for axis in axes )
270+ if smallest_axis < REGULAR_GRID_MIN_POINTS .get (grid_method , 2 ):
271+ warnings .warn (
272+ f"Grid interpolation method '{ grid_method } ' needs at least "
273+ f"{ REGULAR_GRID_MIN_POINTS [grid_method ]} points per axis, but the "
274+ f"coarsest axis of '{ coeff_name } ' has { smallest_axis } ; falling "
275+ "back to 'linear'." ,
276+ UserWarning ,
277+ )
278+ grid_method = "linear"
279+ if grid_method != "linear" :
280+ grid_function ._grid_method = grid_method
281+ grid_function .set_interpolation ("regular_grid" )
282+ grid_function .set_extrapolation (grid_function .get_extrapolation_method ())
283+ return grid_function
225284
226285 # Define all set methods
227286 def set_inputs (self , inputs ):
@@ -318,6 +377,10 @@ def set_source(self, source): # pylint: disable=too-many-statements
318377 self .__dom_dim__ = source .shape [1 ] - 1
319378 self ._domain = source [:, :- 1 ]
320379 self ._image = source [:, - 1 ]
380+ # Cache per-dimension domain bounds so the N-D hot evaluation path
381+ # (``__get_value_opt_nd``) does not recompute them on every call.
382+ self ._domain_min = self ._domain .min (axis = 0 )
383+ self ._domain_max = self ._domain .max (axis = 0 )
321384
322385 # set x and y. If Function is 2D, also set z
323386 if self .__dom_dim__ == 1 :
@@ -488,11 +551,20 @@ def __process_grid_source(self, source):
488551 f"{ grid_data .shape [i ]} points."
489552 )
490553 if not np .all (np .diff (ax ) > 0 ):
491- warnings .warn (
492- f"Axis { i } is not strictly sorted in ascending order. "
493- "RegularGridInterpolator requires sorted axes." ,
494- UserWarning ,
495- )
554+ # RegularGridInterpolator requires strictly ascending axes. Sort
555+ # this axis (and reorder the grid data along it) so descending or
556+ # shuffled inputs are accepted; repeated coordinates cannot form
557+ # a regular grid and are rejected with a clear error rather than
558+ # a cryptic SciPy failure.
559+ order = np .argsort (ax , kind = "stable" )
560+ ax = ax [order ]
561+ grid_data = np .take (grid_data , order , axis = i )
562+ axes [i ] = ax
563+ if not np .all (np .diff (ax ) > 0 ):
564+ raise ValueError (
565+ f"Axis { i } has repeated coordinates; a regular grid "
566+ "requires strictly increasing values along each axis."
567+ )
496568
497569 self ._grid_axes = axes
498570 self ._grid_data = grid_data
@@ -596,7 +668,7 @@ def rbf_interpolation(x, x_min, x_max, x_data, y_data, coeffs): # pylint: disab
596668 grid_interpolator = RegularGridInterpolator (
597669 self ._grid_axes ,
598670 self ._grid_data ,
599- method = " linear" ,
671+ method = getattr ( self , "_grid_method" , " linear") ,
600672 bounds_error = True ,
601673 )
602674 # Store so extrapolation funcs can reuse it
@@ -720,9 +792,9 @@ def natural_extrapolation( # pylint: disable=function-redefined
720792 grid_extrapolator = RegularGridInterpolator (
721793 self ._grid_axes ,
722794 self ._grid_data ,
723- method = " linear" ,
795+ method = getattr ( self , "_grid_method" , " linear") ,
724796 bounds_error = False ,
725- fill_value = None , # linear extrapolation beyond edges
797+ fill_value = None , # extrapolation beyond edges
726798 )
727799
728800 def natural_extrapolation ( # pylint: disable=function-redefined
@@ -824,8 +896,15 @@ def __get_value_opt_nd(self, *args):
824896 arg_qty = len (args )
825897 result = np .empty (arg_qty )
826898
827- min_domain = self ._domain .T .min (axis = 1 )
828- max_domain = self ._domain .T .max (axis = 1 )
899+ # Domain bounds are fixed once the source is set, so they are cached in
900+ # ``set_source`` (this hot path runs per integration step); fall back to
901+ # computing them for any Function built without going through it.
902+ min_domain = getattr (self , "_domain_min" , None )
903+ if min_domain is None :
904+ min_domain = self ._domain .min (axis = 0 )
905+ max_domain = self ._domain .max (axis = 0 )
906+ else :
907+ max_domain = self ._domain_max
829908
830909 lower , upper = args < min_domain , args > max_domain
831910 extrap = np .logical_or (lower .any (axis = 1 ), upper .any (axis = 1 ))
@@ -4162,7 +4241,7 @@ def to_dict(self, **kwargs): # pylint: disable=unused-argument
41624241 else :
41634242 source = source .__name__
41644243
4165- return {
4244+ function_dict = {
41664245 "source" : source ,
41674246 "title" : self .title ,
41684247 "inputs" : self .__inputs__ ,
@@ -4171,6 +4250,20 @@ def to_dict(self, **kwargs): # pylint: disable=unused-argument
41714250 "extrapolation" : self .__extrapolation__ ,
41724251 }
41734252
4253+ # A regular-grid Function cannot be rebuilt from its flat scatter
4254+ # ``source``; persist the ``(axes, grid_data)`` structure (and the mapped
4255+ # scipy method) instead, so it round-trips through ``from_dict``.
4256+ if self .__interpolation__ == "regular_grid" :
4257+ function_dict ["source" ] = [
4258+ [np .asarray (axis ).tolist () for axis in self ._grid_axes ],
4259+ np .asarray (self ._grid_data ).tolist (),
4260+ ]
4261+ grid_method = getattr (self , "_grid_method" , "linear" )
4262+ if grid_method != "linear" :
4263+ function_dict ["grid_method" ] = grid_method
4264+
4265+ return function_dict
4266+
41744267 @classmethod
41754268 def from_dict (cls , func_dict ):
41764269 """Creates a Function instance from a dictionary.
@@ -4184,7 +4277,7 @@ def from_dict(cls, func_dict):
41844277 if func_dict ["interpolation" ] is None and func_dict ["extrapolation" ] is None :
41854278 source = from_hex_decode (source )
41864279
4187- return cls (
4280+ function = cls (
41884281 source = source ,
41894282 interpolation = func_dict ["interpolation" ],
41904283 extrapolation = func_dict ["extrapolation" ],
@@ -4193,6 +4286,16 @@ def from_dict(cls, func_dict):
41934286 title = func_dict ["title" ],
41944287 )
41954288
4289+ # Restore a non-default regular-grid method (the constructor above builds
4290+ # the "linear" default); rebuild the interpolator/extrapolator with it.
4291+ grid_method = func_dict .get ("grid_method" )
4292+ if grid_method and grid_method != "linear" :
4293+ function ._grid_method = grid_method
4294+ function .set_interpolation ("regular_grid" )
4295+ function .set_extrapolation (function .get_extrapolation_method ())
4296+
4297+ return function
4298+
41964299 @staticmethod
41974300 def __make_arith_lambda (
41984301 operator , func , other , func_dim , other_dim = 0 , reverse = False
0 commit comments