agd.Interpolation

This file implements some spline interpolation methods, on uniform grids, in a manner compatible with automatic differentiation.

If none of the involved arrays use automatic differentiation, and if the options are compatible, then a bypass through ndimage may be used.

  1# Copyright 2020 Jean-Marie Mirebeau, University Paris-Sud, CNRS, University Paris-Saclay
  2# Distributed WITHOUT ANY WARRANTY. Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0
  3
  4"""
  5This file implements some spline interpolation methods, on uniform grids,
  6in a manner compatible with automatic differentiation.
  7
  8If none of the involved arrays use automatic differentiation, and if the options are 
  9compatible, then a bypass through ndimage may be used.
 10"""
 11
 12
 13import numpy as np
 14import itertools
 15import scipy.linalg
 16
 17from . import AutomaticDifferentiation as ad
 18
 19#TODO : use a smaller stencil (3 pts instead of 4) for 2nd degree interpolation 
 20# when far from the boundary, in non-periodic mode. (Introduce interior nodes.)
 21
 22def origin_scale_shape(grid):
 23	"""
 24	This function is indended for extracting the origin, scale, and shape,
 25	of a uniform coordinate system provided as a meshgrid.
 26	"""
 27	def _orig_step_len(a,axis):
 28		a = ad.asarray(a)
 29		assert a.ndim==1 or a.ndim>=axis
 30		if a.ndim>1:a=a.__getitem__((0,)*axis+(slice(None,),)+(0,)*(a.ndim-axis-1))
 31		return a[0],a[1]-a[0],len(a)
 32	origin_scale_shape = [_orig_step_len(a,axis) for axis,a in enumerate(grid)]
 33	origin,scale,shape = [list(l) for l in zip(*origin_scale_shape)]
 34	caster = ad.cupy_generic.array_float_caster(grid,iterables=(list,tuple))
 35	return caster(origin),caster(scale),tuple(shape)
 36
 37
 38def _append_dims(x,ndim):
 39	"""Appends specified number of singleton dimensions"""
 40	return np.reshape(x,x.shape+(1,)*ndim)
 41
 42def map_coordinates(input,coordinates,*args,
 43	grid=None,origin=None,scale=None,depth=0,order=1,**kwargs):
 44	"""
 45	Thin wrapper over the ndimage.map_coordinates function, which adds the possibility of 
 46	rescaling the coordinates using a reference grid, and interpolating tensors.
 47	Will dispatch to cupyx.scipy.ndimage.map_coordinates if needed.
 48
 49	Additional inputs : 
 50	- grid (optional) : reference coordinate system, which must be uniform
 51	- origin,scale (optional) : obtained from origin_scale_shape(grid)
 52	- depth : depth of interpolated objects 0->scalar, 1->vector, 2->matrix
 53	- order (optional) : set default 1 for better cupy/numpy reproducibility
 54	"""
 55	kwargs['order']=order
 56	if ad.cupy_generic.from_cupy(input):
 57		from cupyx.scipy.ndimage import map_coordinates as mc
 58		def map_coordinates(arr,x,*args,**kwargs):
 59			# Cupy (version 7.8) requires the coordinates array to be flattened
 60			shape = x.shape[1:]
 61			return mc(arr,x.reshape((len(x),-1)),*args,**kwargs).reshape(shape)
 62	else: from scipy.ndimage import map_coordinates
 63
 64	if grid is not None: origin,scale,_ = origin_scale_shape(grid)
 65	assert (origin is None) == (scale is None)
 66	if origin is None: return map_coordinates(input,coordinates,*args,**kwargs)
 67	origin,scale = (_append_dims(e,coordinates.ndim-1) for e in (origin,scale))
 68	y = (coordinates-origin)/scale
 69
 70	if depth==0: return map_coordinates(input,y,*args,**kwargs)
 71	oshape = input.shape[:depth]
 72	input = input.reshape((-1,)+input.shape[depth:])
 73	out = ad.array([map_coordinates(input_,y,*args,**kwargs) for input_ in input])
 74	out.reshape(oshape+y.shape[1:])
 75	return out
 76
 77class _spline_univariate:
 78	"""
 79	A univariate spline of a given order, with not-a-knot boundary conditions.
 80	"""
 81	def __init__(self,order,shape,periodic):
 82		assert isinstance(order,int)
 83		self.order=order
 84		if not (order>=1 and order<=3):
 85			raise ValueError("Unsupported spline order")
 86
 87		assert isinstance(shape,int) and shape>=0
 88		self.shape = shape
 89
 90		assert isinstance(periodic,bool)
 91		self.periodic=periodic
 92
 93
 94	def __call__(self,xa,xs):
 95		"""
 96		Weight at absolute position xa, of of spline centered at xs.
 97		"""
 98		xa=ad.asarray(xa)
 99		xs=ad.asarray(xs)
100		if   self.order==1: return self._call1(xa,xs)
101		elif self.order==2: return self._call2(xa,xs)
102		elif self.order==3: return self._call3(xa,xs)
103		else: assert False
104
105	def nodes(self,interior):
106		"""
107		Returns range(a,b) where [x+a,x+b] contains the support
108		 of the spline centered at some point x (interior or boundary) 
109		"""
110		if   self.order==1: return range(-1,1)
111		elif self.order==2: #return range(-1,2)
112			return range(-1,2) if interior else range(-2,2)
113		elif self.order==3: 
114			return range(-2,2) if interior else range(-3,4)
115		assert False
116
117	def interior(self,x,tol=None):
118		"""
119		Wether the interior nodes can be used, or one should fall back to boundary nodes.
120		"""
121		if tol is None: tol = ad.precision(x) * np.max(self.shape)
122		if np.any(x<-tol) or np.any(x>self.shape-1+tol):
123			raise ValueError("Interpolating data outside domain")
124		if self.order==1 or self.periodic:
125			return np.full_like(x,True,dtype='bool')
126		elif self.order==2:
127			return x>=1
128		elif self.order==3:
129			return np.logical_and(x>=1,x<=self.shape-2)
130
131	def _call1(self,xa,xs):
132		"""
133		A piecewise linear spline, defined over [-1,1].
134		"""
135		x=xa-xs.astype(xa.dtype) # Avoid float32 + int32 -> float64 cast on GPU
136		result = np.zeros_like(x)
137		seg=ad.asarray(np.floor(x+1))
138		
139		# relative interval [-1,0[
140		pos = seg==0
141		result[pos] = 1+x[pos]
142
143		# relative interval [0,1[
144		pos = seg==1
145		result[pos] = 1-x[pos]
146
147		return result
148
149		# Rejected because derivative at node points is inconsistent accross nodes
150		# return np.maximum(0.,1.-np.abs(x))
151
152	def _call2(self,xa,xs):
153		"""
154		A piecewise quadratic spline function, defined over [-1,2]
155		"""
156		x=ad.asarray(xa-xs)
157		result = np.zeros_like(x)
158
159
160		# Which spline segment to use
161		seg = ad.asarray(np.floor(x+1))
162
163		if not self.periodic:
164			# Implements the not-a-knot boundary condition
165			pos = np.logical_and(xa<=1,xs<=3)
166			seg[pos] = 2-xs[pos]
167
168		# relative interval [-1,0[
169		pos = seg==0
170		x_ = x[pos]+1
171		result[pos] = x_**2
172
173		# relative interval [0,1[
174		pos = seg==1
175		x_ = x[pos]-0.5
176		result[pos] = 1.5 - 2.*x_**2
177
178		# relative interval [1,2[
179		pos = seg==2
180		x_ = 2.-x[pos]
181		result[pos] = x_**2 
182
183		return result
184
185	def _call3(self,xa,xs):
186		"""
187		A piecewise cubic spline function, defined over [-2,2]
188		"""
189		x=ad.asarray(xa-xs)
190		result = np.zeros_like(x)
191		s=self.shape-1
192
193		# Which spline segment to use
194		seg = ad.asarray(np.floor(x+2))
195		if not self.periodic:
196			# Implements the not-a-knot boundary condition
197			pos = np.logical_and(xa<=1,xs<=3)
198			seg[pos] = 3-xs[pos]
199			pos = np.logical_and(xa>=s-1,xs>=s-3)
200			seg[pos] = self.shape-1-xs[pos]
201
202		def f0(y): return y**3
203		def f1(y): return 1+3*y+3*y**2-3.*y**3
204
205		# relative interval [-2,-1[
206		pos = seg==0
207		result[pos] = f0(x[pos]+2.)
208
209		# relative interval [-1,0[
210		pos = seg==1
211		result[pos] = f1(x[pos]+1.)
212
213		# relative interval [0,1[
214		pos = seg==2
215		result[pos] = f1(1.-x[pos])
216
217		# relative interval [1,2[
218		pos = seg==3
219		result[pos] = f0(2.-x[pos])
220		
221		"""
222		if not self.periodic:
223			# End of not-a-knot boundary condition
224			def g(y): return 4.-6.*y**2+(50./27.)*y**3
225			xa=ad.broadcast_to(xa,x.shape)
226			pos = np.logical_and(xa<=3,xs==3)
227			result[pos] = g(3.-xa[pos])
228			pos = np.logical_and(xa>s-3,xs==s-3)
229			result[pos] = g(xa[pos]-(s-3))
230		"""
231
232		return result
233
234	def _band(self):
235		rg = np.arange(self.shape+0.)
236		if self.order==2:
237			band_tr = np.stack((self(rg,rg-1),self(rg,rg),self(rg,rg+1),self(rg,rg+2)),axis=0)
238			return _banded_transpose((2,1),band_tr)
239		elif self.order==3:
240			band_tr = np.stack((self(rg,rg-3),self(rg,rg-2),self(rg,rg-1),
241				self(rg,rg),self(rg,rg+1),self(rg,rg+2),self(rg,rg+3)),axis=0)
242			return _banded_transpose((3,3),band_tr)
243		else: assert False
244
245
246	def make_coefs(self,values,overwrite_values=False):
247		"""
248		Produces the node coefficients corresponding to given values.
249		!! Call convention : interpolation is along the first axis. !!
250		"""
251		if self.order==1: return values
252		if self.periodic: raise ValueError("Periodic interpolation is not supported for degree > 1")
253		assert len(values)==self.shape
254
255		def solver(vals): return scipy.linalg.solve_banded(*self._band(),vals,
256				overwrite_ab=True,overwrite_b=overwrite_values)
257
258		if ad.is_ad(values): return values.apply_linear_operator(solver)
259#			raise ValueError("AD interpolation is not supported for degree > 1")
260
261		return solver(values)
262		
263def _banded_transpose(lu,t):
264	l,u=lu
265	return (u,l),np.stack(tuple(np.roll(t[i],i-u) for i in reversed(range(l+u+1))))
266#	return np.stack((np.roll(t[2],1),t[1],np.roll(t[0],-1)),axis=0)
267
268def _banded_densify(lu,t):
269	"""Turn banded matrix in to dense matrix (inefficient, for testing)"""
270	l,u=lu
271	n = t.shape[1]
272	mat = np.zeros((n,n))
273	for i in range(n):
274		for j in range(n):
275			k=u+i-j
276			if 0<=k and k<len(t):
277				mat[i,j]=t[k,j]
278	return mat
279
280class _spline_tensor:
281	"""
282	A tensor product of univariate splines.
283	"""
284	def __init__(self,orders,shape,periodic):
285		assert all(isinstance(t,tuple) for t in (orders,shape,periodic))
286		self.splines = tuple(_spline_univariate(order,s,per) 
287			for (order,s,per) in zip(orders,shape,periodic))
288		assert all(len(t)==self.vdim for t in (orders,shape,periodic))
289
290	@property
291	def order(self):
292		return tuple(spline.order for spline in self.splines)
293	@property
294	def shape(self):
295		return tuple(spline.shape for spline in self.splines)
296	@property
297	def periodic(self):
298		return tuple(spline.periodic for spline in self.splines)
299	
300	@property
301	def vdim(self):
302		return len(self.splines)
303
304	def __call__(self,xa,xs):
305		"""
306		Weight at absolute position xa, of of spline centered at xs.
307		"""
308		return np.prod( ad.asarray(tuple(spline(xai,xsi) 
309			for (xai,xsi,spline) in zip(xa,xs,self.splines))), axis=0)
310
311	def nodes(self,interior):
312		"""
313		The spline at x (interior or boundary) is supported 
314		on the union of x+node+[0,1]**vdim
315		"""
316		_nodes = tuple(spline.nodes(interior) for spline in self.splines)
317		return np.asarray(tuple(itertools.product(*_nodes))).T
318
319	def interior(self,x):
320		"""
321		Wether the interior nodes can be used.
322		"""
323		return ad.asarray(tuple(spline.interior(xi) 
324			for (spline,xi) in zip(self.splines,x))).all(axis=0)
325#		return np.logical_and.reduce(tuple(spline.interior(xi) # cupy has no reduce
326#			for (spline,xi) in zip(self.splines,x)))
327
328	def make_coefs(self,values,overwrite_values=False):
329		"""
330		Produces the node coefficients corresponding to given values.
331		!! Call convention : interpolation is along the first axes. 
332		"""
333		for i,spline in enumerate(self.splines): #reversed(list(enumerate(self.splines))):
334			values = np.moveaxis(spline.make_coefs(
335				np.moveaxis(values,i,0),overwrite_values=overwrite_values),0,i)
336		return values
337
338class UniformGridInterpolation:
339	"""
340	Interpolates values on a uniform grid, in arbitrary dimension, using splines of 
341	a given order.
342	"""
343
344	def __init__(self,grid,values=None,order=1,periodic=False,check_grid=True):
345		"""
346		- grid (ndarray) : must be a uniform grid. E.g. np.meshgrid(aX,aY,indexing='ij')
347		 where aX,aY have uniform spacing. Alternatively, provide only the axes.
348		- values (ndarray) : interpolated values.
349		- order (int, tuple of ints) : spline interpolation order (<=3), along each axis.
350		- periodic (bool, tuple of bool) : wether periodic interpolation, along each axis.
351		"""
352		if isinstance(grid,dict):
353			self.shape  = grid['shape']
354			self.origin = ad.asarray(grid['origin'])
355			self.scale  = ad.asarray(grid['scale'])
356			if grid.get('cell_centered',False): 
357				self.origin += self.scale/2 # Convert to node_centered origin
358		else:
359			self.origin,self.scale,self.shape = origin_scale_shape(grid)
360			if check_grid and grid[0].ndim>1:
361				assert np.allclose(grid,self._grid(),atol=1e-5) #Atol allows float32 types
362
363		if order is None: order = 1
364		if isinstance(order,int): order = (order,)*self.vdim
365
366		if periodic is None: periodic=False
367		if isinstance(periodic,bool): periodic = (periodic,)*self.vdim
368		self.periodic=periodic
369
370		self.spline = _spline_tensor(order,self.shape,periodic)
371		assert self.spline.vdim == self.vdim
372		self.interior_nodes = self.spline.nodes(interior=True)
373		self.boundary_nodes = self.spline.nodes(interior=False)
374
375		self.coef = None if values is None else self.make_coefs(values)
376
377	@property
378	def vdim(self):
379		"""
380		Vector dimension of the interpolation points.
381		"""
382		return len(self.shape)
383	@property
384	def oshape(self):
385		"""
386		Number of dimensions of the interpolated objects.
387		"""
388		return self.coef.shape[self.vdim:]
389
390	def __call__(self,x,interior=None):
391		"""
392		Interpolates the data at the position x.
393		"""
394		x=ad.asarray(x)
395		assert len(x)==self.vdim
396		
397		pdim = x.ndim-1 # Number of dimensions of position
398		origin,scale = (_append_dims(e,pdim) for e in (self.origin,self.scale))
399
400		# Separate treatment of interior and boundary points
401		if interior is None:
402			y = (ad.remove_ad(x) - origin)/scale
403			interior_x = self.spline.interior(y)
404			boundary_x = np.logical_not(interior_x)
405			interior_result = self(x[:,interior_x],True)
406			boundary_result = self(x[:,boundary_x],False)
407
408			result_shape = self.oshape+x.shape[1:]
409			if result_shape==tuple(): #numpy zeros_like has a bug for empty shapes
410				some_result = interior_result if interior_result.size>0 else boundary_result
411				result = np.zeros_like(some_result.reshape(-1)[0])
412			else: result = np.zeros_like(interior_result,shape=result_shape)
413
414			try:
415				result[...,interior_x] = interior_result
416				result[...,boundary_x] = boundary_result
417			except (ValueError,IndexError):
418				# Some cupy versions do not handle Ellipsis correctly
419				ellipsis = (slice(None),)*len(self.oshape)
420				result.__setitem__((*ellipsis,interior_x),interior_result)
421				result.__setitem__((*ellipsis,boundary_x),boundary_result)
422
423			return result
424
425		# Rescale the coordinates in reference rectangle
426		y = np.expand_dims((x - origin)/scale,axis=1)
427		# Bottom left pixel
428		yf = np.floor(y).astype(int)
429		# All pixels supporting an active spline
430		nodes = self.interior_nodes if interior else self.boundary_nodes
431		ys = yf - _append_dims(nodes,pdim)
432		
433		# Spline coefficients, taking care of out of domain 
434		ys_ = ys.copy()
435		out = np.full_like(ad.remove_ad(x[0]),False,dtype=bool)
436		for i,(d,per) in enumerate(zip(self.shape,self.periodic)):
437			if per: 
438				ys_[i] = ys_[i]%d
439			else: 
440				bad = np.logical_or(ys_[i]<0,ys_[i]>=d)
441				out = np.logical_or(out,bad)
442				try: 
443					ys_[i,bad] = 0 
444				except ValueError: # Old cupy versions do not support such slices
445					ys_i = ys_[i]
446					ys_i[bad] = 0
447					ys_[i] = ys_i
448
449		coef = self.coef[tuple(ys_)]
450		coef[out]=0.
451		ondim = len(self.oshape)
452		coef = np.moveaxis(coef,range(-ondim,0),range(ondim))
453		
454		# Spline weights
455		weight = self.spline(y,ys)
456		return (coef*weight).sum(axis=ondim)
457
458	def set_values(self,values):
459		self.coef = self.make_coefs(values)
460
461	def make_coefs(self,values,overwrite_values=False):
462		values = ad.asarray(values)
463		xp = ad.cupy_generic.get_array_module(values)
464		self.interior_nodes = xp.array(self.interior_nodes)
465		self.boundary_nodes = xp.array(self.boundary_nodes)
466
467		ondim = values.ndim - self.vdim
468		# Internally, interpolation is along the first axes.
469		# (Contrary to external interface)
470		val = np.moveaxis(values,range(ondim),range(-ondim,0))
471
472		return self.spline.make_coefs(val,overwrite_values=overwrite_values)
473
474	def _grid(self):
475		xp = ad.cupy_generic.get_array_module(self.origin)
476		dtype = self.origin.dtype
477		return ad.asarray(np.meshgrid(*(o+h*xp.arange(s+0.,dtype=dtype) 
478			for (o,h,s) in zip(self.origin,self.scale,self.shape)), indexing='ij'))
def origin_scale_shape(grid):
23def origin_scale_shape(grid):
24	"""
25	This function is indended for extracting the origin, scale, and shape,
26	of a uniform coordinate system provided as a meshgrid.
27	"""
28	def _orig_step_len(a,axis):
29		a = ad.asarray(a)
30		assert a.ndim==1 or a.ndim>=axis
31		if a.ndim>1:a=a.__getitem__((0,)*axis+(slice(None,),)+(0,)*(a.ndim-axis-1))
32		return a[0],a[1]-a[0],len(a)
33	origin_scale_shape = [_orig_step_len(a,axis) for axis,a in enumerate(grid)]
34	origin,scale,shape = [list(l) for l in zip(*origin_scale_shape)]
35	caster = ad.cupy_generic.array_float_caster(grid,iterables=(list,tuple))
36	return caster(origin),caster(scale),tuple(shape)

This function is indended for extracting the origin, scale, and shape, of a uniform coordinate system provided as a meshgrid.

def map_coordinates( input, coordinates, *args, grid=None, origin=None, scale=None, depth=0, order=1, **kwargs):
43def map_coordinates(input,coordinates,*args,
44	grid=None,origin=None,scale=None,depth=0,order=1,**kwargs):
45	"""
46	Thin wrapper over the ndimage.map_coordinates function, which adds the possibility of 
47	rescaling the coordinates using a reference grid, and interpolating tensors.
48	Will dispatch to cupyx.scipy.ndimage.map_coordinates if needed.
49
50	Additional inputs : 
51	- grid (optional) : reference coordinate system, which must be uniform
52	- origin,scale (optional) : obtained from origin_scale_shape(grid)
53	- depth : depth of interpolated objects 0->scalar, 1->vector, 2->matrix
54	- order (optional) : set default 1 for better cupy/numpy reproducibility
55	"""
56	kwargs['order']=order
57	if ad.cupy_generic.from_cupy(input):
58		from cupyx.scipy.ndimage import map_coordinates as mc
59		def map_coordinates(arr,x,*args,**kwargs):
60			# Cupy (version 7.8) requires the coordinates array to be flattened
61			shape = x.shape[1:]
62			return mc(arr,x.reshape((len(x),-1)),*args,**kwargs).reshape(shape)
63	else: from scipy.ndimage import map_coordinates
64
65	if grid is not None: origin,scale,_ = origin_scale_shape(grid)
66	assert (origin is None) == (scale is None)
67	if origin is None: return map_coordinates(input,coordinates,*args,**kwargs)
68	origin,scale = (_append_dims(e,coordinates.ndim-1) for e in (origin,scale))
69	y = (coordinates-origin)/scale
70
71	if depth==0: return map_coordinates(input,y,*args,**kwargs)
72	oshape = input.shape[:depth]
73	input = input.reshape((-1,)+input.shape[depth:])
74	out = ad.array([map_coordinates(input_,y,*args,**kwargs) for input_ in input])
75	out.reshape(oshape+y.shape[1:])
76	return out

Thin wrapper over the ndimage.map_coordinates function, which adds the possibility of rescaling the coordinates using a reference grid, and interpolating tensors. Will dispatch to cupyx.scipy.ndimage.map_coordinates if needed.

Additional inputs :

  • grid (optional) : reference coordinate system, which must be uniform
  • origin,scale (optional) : obtained from origin_scale_shape(grid)
  • depth : depth of interpolated objects 0->scalar, 1->vector, 2->matrix
  • order (optional) : set default 1 for better cupy/numpy reproducibility
class UniformGridInterpolation:
339class UniformGridInterpolation:
340	"""
341	Interpolates values on a uniform grid, in arbitrary dimension, using splines of 
342	a given order.
343	"""
344
345	def __init__(self,grid,values=None,order=1,periodic=False,check_grid=True):
346		"""
347		- grid (ndarray) : must be a uniform grid. E.g. np.meshgrid(aX,aY,indexing='ij')
348		 where aX,aY have uniform spacing. Alternatively, provide only the axes.
349		- values (ndarray) : interpolated values.
350		- order (int, tuple of ints) : spline interpolation order (<=3), along each axis.
351		- periodic (bool, tuple of bool) : wether periodic interpolation, along each axis.
352		"""
353		if isinstance(grid,dict):
354			self.shape  = grid['shape']
355			self.origin = ad.asarray(grid['origin'])
356			self.scale  = ad.asarray(grid['scale'])
357			if grid.get('cell_centered',False): 
358				self.origin += self.scale/2 # Convert to node_centered origin
359		else:
360			self.origin,self.scale,self.shape = origin_scale_shape(grid)
361			if check_grid and grid[0].ndim>1:
362				assert np.allclose(grid,self._grid(),atol=1e-5) #Atol allows float32 types
363
364		if order is None: order = 1
365		if isinstance(order,int): order = (order,)*self.vdim
366
367		if periodic is None: periodic=False
368		if isinstance(periodic,bool): periodic = (periodic,)*self.vdim
369		self.periodic=periodic
370
371		self.spline = _spline_tensor(order,self.shape,periodic)
372		assert self.spline.vdim == self.vdim
373		self.interior_nodes = self.spline.nodes(interior=True)
374		self.boundary_nodes = self.spline.nodes(interior=False)
375
376		self.coef = None if values is None else self.make_coefs(values)
377
378	@property
379	def vdim(self):
380		"""
381		Vector dimension of the interpolation points.
382		"""
383		return len(self.shape)
384	@property
385	def oshape(self):
386		"""
387		Number of dimensions of the interpolated objects.
388		"""
389		return self.coef.shape[self.vdim:]
390
391	def __call__(self,x,interior=None):
392		"""
393		Interpolates the data at the position x.
394		"""
395		x=ad.asarray(x)
396		assert len(x)==self.vdim
397		
398		pdim = x.ndim-1 # Number of dimensions of position
399		origin,scale = (_append_dims(e,pdim) for e in (self.origin,self.scale))
400
401		# Separate treatment of interior and boundary points
402		if interior is None:
403			y = (ad.remove_ad(x) - origin)/scale
404			interior_x = self.spline.interior(y)
405			boundary_x = np.logical_not(interior_x)
406			interior_result = self(x[:,interior_x],True)
407			boundary_result = self(x[:,boundary_x],False)
408
409			result_shape = self.oshape+x.shape[1:]
410			if result_shape==tuple(): #numpy zeros_like has a bug for empty shapes
411				some_result = interior_result if interior_result.size>0 else boundary_result
412				result = np.zeros_like(some_result.reshape(-1)[0])
413			else: result = np.zeros_like(interior_result,shape=result_shape)
414
415			try:
416				result[...,interior_x] = interior_result
417				result[...,boundary_x] = boundary_result
418			except (ValueError,IndexError):
419				# Some cupy versions do not handle Ellipsis correctly
420				ellipsis = (slice(None),)*len(self.oshape)
421				result.__setitem__((*ellipsis,interior_x),interior_result)
422				result.__setitem__((*ellipsis,boundary_x),boundary_result)
423
424			return result
425
426		# Rescale the coordinates in reference rectangle
427		y = np.expand_dims((x - origin)/scale,axis=1)
428		# Bottom left pixel
429		yf = np.floor(y).astype(int)
430		# All pixels supporting an active spline
431		nodes = self.interior_nodes if interior else self.boundary_nodes
432		ys = yf - _append_dims(nodes,pdim)
433		
434		# Spline coefficients, taking care of out of domain 
435		ys_ = ys.copy()
436		out = np.full_like(ad.remove_ad(x[0]),False,dtype=bool)
437		for i,(d,per) in enumerate(zip(self.shape,self.periodic)):
438			if per: 
439				ys_[i] = ys_[i]%d
440			else: 
441				bad = np.logical_or(ys_[i]<0,ys_[i]>=d)
442				out = np.logical_or(out,bad)
443				try: 
444					ys_[i,bad] = 0 
445				except ValueError: # Old cupy versions do not support such slices
446					ys_i = ys_[i]
447					ys_i[bad] = 0
448					ys_[i] = ys_i
449
450		coef = self.coef[tuple(ys_)]
451		coef[out]=0.
452		ondim = len(self.oshape)
453		coef = np.moveaxis(coef,range(-ondim,0),range(ondim))
454		
455		# Spline weights
456		weight = self.spline(y,ys)
457		return (coef*weight).sum(axis=ondim)
458
459	def set_values(self,values):
460		self.coef = self.make_coefs(values)
461
462	def make_coefs(self,values,overwrite_values=False):
463		values = ad.asarray(values)
464		xp = ad.cupy_generic.get_array_module(values)
465		self.interior_nodes = xp.array(self.interior_nodes)
466		self.boundary_nodes = xp.array(self.boundary_nodes)
467
468		ondim = values.ndim - self.vdim
469		# Internally, interpolation is along the first axes.
470		# (Contrary to external interface)
471		val = np.moveaxis(values,range(ondim),range(-ondim,0))
472
473		return self.spline.make_coefs(val,overwrite_values=overwrite_values)
474
475	def _grid(self):
476		xp = ad.cupy_generic.get_array_module(self.origin)
477		dtype = self.origin.dtype
478		return ad.asarray(np.meshgrid(*(o+h*xp.arange(s+0.,dtype=dtype) 
479			for (o,h,s) in zip(self.origin,self.scale,self.shape)), indexing='ij'))

Interpolates values on a uniform grid, in arbitrary dimension, using splines of a given order.

UniformGridInterpolation(grid, values=None, order=1, periodic=False, check_grid=True)
345	def __init__(self,grid,values=None,order=1,periodic=False,check_grid=True):
346		"""
347		- grid (ndarray) : must be a uniform grid. E.g. np.meshgrid(aX,aY,indexing='ij')
348		 where aX,aY have uniform spacing. Alternatively, provide only the axes.
349		- values (ndarray) : interpolated values.
350		- order (int, tuple of ints) : spline interpolation order (<=3), along each axis.
351		- periodic (bool, tuple of bool) : wether periodic interpolation, along each axis.
352		"""
353		if isinstance(grid,dict):
354			self.shape  = grid['shape']
355			self.origin = ad.asarray(grid['origin'])
356			self.scale  = ad.asarray(grid['scale'])
357			if grid.get('cell_centered',False): 
358				self.origin += self.scale/2 # Convert to node_centered origin
359		else:
360			self.origin,self.scale,self.shape = origin_scale_shape(grid)
361			if check_grid and grid[0].ndim>1:
362				assert np.allclose(grid,self._grid(),atol=1e-5) #Atol allows float32 types
363
364		if order is None: order = 1
365		if isinstance(order,int): order = (order,)*self.vdim
366
367		if periodic is None: periodic=False
368		if isinstance(periodic,bool): periodic = (periodic,)*self.vdim
369		self.periodic=periodic
370
371		self.spline = _spline_tensor(order,self.shape,periodic)
372		assert self.spline.vdim == self.vdim
373		self.interior_nodes = self.spline.nodes(interior=True)
374		self.boundary_nodes = self.spline.nodes(interior=False)
375
376		self.coef = None if values is None else self.make_coefs(values)
  • grid (ndarray) : must be a uniform grid. E.g. np.meshgrid(aX,aY,indexing='ij') where aX,aY have uniform spacing. Alternatively, provide only the axes.
  • values (ndarray) : interpolated values.
  • order (int, tuple of ints) : spline interpolation order (<=3), along each axis.
  • periodic (bool, tuple of bool) : wether periodic interpolation, along each axis.
periodic
spline
interior_nodes
boundary_nodes
coef
vdim
378	@property
379	def vdim(self):
380		"""
381		Vector dimension of the interpolation points.
382		"""
383		return len(self.shape)

Vector dimension of the interpolation points.

oshape
384	@property
385	def oshape(self):
386		"""
387		Number of dimensions of the interpolated objects.
388		"""
389		return self.coef.shape[self.vdim:]

Number of dimensions of the interpolated objects.

def set_values(self, values):
459	def set_values(self,values):
460		self.coef = self.make_coefs(values)
def make_coefs(self, values, overwrite_values=False):
462	def make_coefs(self,values,overwrite_values=False):
463		values = ad.asarray(values)
464		xp = ad.cupy_generic.get_array_module(values)
465		self.interior_nodes = xp.array(self.interior_nodes)
466		self.boundary_nodes = xp.array(self.boundary_nodes)
467
468		ondim = values.ndim - self.vdim
469		# Internally, interpolation is along the first axes.
470		# (Contrary to external interface)
471		val = np.moveaxis(values,range(ondim),range(-ondim,0))
472
473		return self.spline.make_coefs(val,overwrite_values=overwrite_values)