agd.FiniteDifferences

This module implements some finite differences operations, as well as related array reshaping and broadcasting operations. The main functions are the following, see the the detailed help below.

Elementary finite differences:

  • DiffUpwind
  • DiffCentered
  • Diff2

Array broadcasting:

  • as_field
  • common_field

Block based array reshaping:

  • block_expand
  • block_squeeze
  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 module implements some finite differences operations, as well as related array 
  6reshaping and broadcasting operations. The main functions are the following, see the 
  7the detailed help below.
  8
  9Elementary finite differences:
 10- DiffUpwind
 11- DiffCentered
 12- Diff2
 13
 14Array broadcasting:
 15- as_field
 16- common_field
 17
 18Block based array reshaping:
 19- block_expand
 20- block_squeeze
 21
 22"""
 23
 24import numpy as np
 25import itertools
 26from . import AutomaticDifferentiation as ad
 27import functools
 28import operator
 29
 30# --- Domain shape related methods --------
 31
 32def as_field(u,shape,conditional=True,depth=None):
 33	"""
 34	Checks if the last dimensions of u match the given shape. 
 35	If not, u is extended with these additional dimensions.
 36	conditional : if False, reshaping is always done
 37	depth (optional) : the depth of the geometrical tensor field (1: vectors, 2: matrices)
 38	"""
 39	u=ad.asarray(u)
 40	ndim = len(shape)
 41	def as_is():
 42		if not conditional: return False
 43		elif depth is None: return u.ndim>=ndim and u.shape[-ndim:]==shape
 44		else: assert u.shape[depth:] in (tuple(),shape); return u.shape[depth:]==shape
 45	if as_is(): return u
 46	else: return np.broadcast_to(u.reshape(u.shape+(1,)*ndim), u.shape+shape)
 47
 48def common_shape(arrays,depths):
 49	"""Finds a common shape to the arrays for broadcasting"""
 50	arrays = tuple(None if arr is None else ad.asarray(arr) for arr in arrays)
 51	assert len(arrays)==len(depths)
 52	for arr,depth in zip(arrays,depths):
 53		if arr is None: continue
 54		shape = arr.shape[depth:]
 55		if shape!=tuple(): break
 56	for arr,depth in zip(arrays,depths): 
 57		assert arr is None or arr.shape[depth:] in (tuple(),shape)
 58	return shape
 59
 60def common_field(arrays,depths,shape=None):
 61	"""
 62	Adds trailing dimensions, and broadcasts the given arrays, for suitable interoperation.
 63	
 64	Inputs: 
 65	- arrays : a list [a_1,...,a_n], or iterable, of numeric arrays such that
 66	 a_i.shape = shape_i + shape, or a_i.shape = shape_i, for each 1<=i<=n.
 67	- depths : defined as [len(shape_i) for 1<=i<=n]
 68	- shape (optional) : the trailing shape.
 69	
 70	Output:
 71	- the arrays, with added trailing and dimensions broadcasting so that
 72	 a_i.shape = shape_i + shape for each 1<=i<=n.
 73
 74	"""
 75	arrays = tuple(None if arr is None else ad.asarray(arr) for arr in arrays)
 76	assert len(arrays)==len(depths)
 77	if shape is None: shape = common_shape(arrays,depths)
 78	return tuple(None if arr is None else as_field(arr,shape,depth=depth) 
 79		for (arr,depth) in zip(arrays,depths))
 80
 81def round_up_ratio(num,den):
 82	"""
 83	Returns the least multiple of den after num.
 84	num and den must be integers, with den>0. 
 85	"""
 86	num,den = np.asarray(num),np.asarray(den)
 87	return (num+den-1)//den
 88
 89def block_expand(arr,shape_i,shape_o=None,**kwargs):
 90	"""
 91	Reshape an array so as to factor shape_i (the inner shape),
 92	and move these axes last.
 93	Inputs : 
 94	 - **kwargs : passed to np.pad
 95	Output : 
 96	 - padded and reshaped array
 97	"""
 98	ndim = len(shape_i)
 99	if ndim==0: return arr # Empty block
100	shape_pre = arr.shape[:-ndim]
101	ndim_pre = len(shape_pre)
102	shape_tot=np.array(arr.shape[-ndim:])
103	shape_i = np.array(shape_i)
104
105	# Extend data
106	if shape_o is None: shape_o = round_up_ratio(shape_tot,shape_i)
107	shape_pad = (0,)*ndim_pre + tuple(shape_o*shape_i - shape_tot)
108	arr = np.pad(arr, tuple( (0,s) for s in shape_pad), **kwargs) 
109
110	# Reshape
111	shape_interleaved = tuple(np.stack( (shape_o,shape_i), axis=1).flatten())
112	arr = arr.reshape(shape_pre + shape_interleaved)
113
114	# Move axes
115	rg = np.arange(ndim)
116	axes_interleaved = ndim_pre + 1+2*rg
117	axes_split = ndim_pre + ndim+rg
118	arr = np.moveaxis(arr,axes_interleaved,axes_split)
119
120	return arr
121
122def block_squeeze(arr,shape):
123	"""
124	Inverse operation to block_expand.
125	"""
126	ndim = len(shape)
127	if ndim==0: return arr # Empty block
128	shape_pre = arr.shape[:-2*ndim]
129	ndim_pre = len(shape_pre)
130	shape_o = arr.shape[(-2*ndim):-ndim]
131	shape_i = arr.shape[-ndim:]
132
133	# Move axes
134	rg = np.arange(ndim)
135	axes_interleaved = ndim_pre + 1+2*rg
136	axes_split = ndim_pre + ndim+rg
137	arr = np.moveaxis(arr,axes_split,axes_interleaved)
138
139	# Reshape
140	arr = arr.reshape(shape_pre
141		+tuple(s_o*s_i for (s_o,s_i) in zip(shape_o,shape_i)) )
142
143	# Extract subdomain
144	region = tuple(slice(0,s) for s in (shape_pre+shape))
145	arr = arr.__getitem__(region)
146
147	return arr
148
149def block_neighbors(shape_i,top=True):
150	"""
151	The first layer of neighbors to a block, along the top or bottom, ordered 
152	by increasing indices. Used for efficient data load when a implementing 
153	gradients, divergences, etc, with a compact scheme
154	"""
155	shape_i = np.array(shape_i); vdim=len(shape_i); size_i = np.prod(shape_i)
156	aX = [np.arange(s+1) if top else np.arange(s-1,2*s) for s in shape_i]
157	x_e = np.array(np.meshgrid(*aX)).astype(int)
158	x_e = np.moveaxis(x_e.reshape((vdim,-1)),0,-1)
159	x_e = np.array([x for x in x_e if np.any(x==(shape_i if top else shape_i-1))])
160	ind = np.arange(2**vdim*size_i).reshape((2,)*vdim+tuple(shape_i))
161	ind = block_squeeze(ind,tuple(2*shape_i))
162	ind_x = np.array([ind[tuple(x)] for x in x_e])
163	x_e = x_e[np.argsort(ind_x)]
164	#print(np.array([ind[tuple(x)] for x in x_e]))
165	if not top: x_e-=shape_i
166	return x_e
167
168
169# ----- Utilities for finite differences ------
170
171def BoundedSlices(slices,shape):
172	"""
173	Returns the input slices with None replaced with the upper bound
174	from the given shape
175	"""
176	if slices[-1]==Ellipsis:
177		slices=slices[:-1]+(slice(None,None,None),)*(len(shape)-len(slices)+1)
178	def BoundedSlice(s,n):
179		if not isinstance(s,slice):
180			return slice(s,s+1)
181		else:
182			return slice(s.start, n if s.stop is None else s.stop, s.step)
183	return tuple(BoundedSlice(s,n) for s,n in zip(slices,shape))
184
185def OffsetToIndex(shape,offset, mode='clip', uniform=None, where=(Ellipsis,)):
186	"""
187	Returns the index corresponding to position + offset, 
188	and a boolean for wether it falls inside the domain.
189	"""
190	ndim = len(shape) # Domain dimension
191	assert(offset.shape[0]==ndim)
192	if uniform is None: # Uniform = True iff offsets are independent of the position in the domain
193		uniform = not ((offset.ndim > ndim) and (offset.shape[-ndim:]==shape))
194
195	odim = (offset.ndim-1) if uniform else (offset.ndim - ndim-1) # Dimensions for distinct offsets 
196	everywhere = where==(Ellipsis,)
197	xp=ad.cupy_generic.get_array_module(offset)
198
199	grid = (xp.mgrid[tuple(slice(n) for n in shape)]
200		if everywhere else
201		np.squeeze(xp.mgrid[BoundedSlices(where,shape)],
202		tuple(1+i for i,s in enumerate(where) if not isinstance(s,slice)) ) )
203	grid = grid.reshape( (ndim,) + (1,)*odim+grid.shape[1:])
204
205	if not everywhere and not uniform:
206		offset = offset[(slice(None),)*(1+odim)+where]
207
208	neigh = grid + (offset.reshape(offset.shape + (1,)*ndim) if uniform else offset)
209	bound = xp.array(shape,dtype=neigh.dtype).reshape((ndim,)+(1,)*(neigh.ndim-1))
210
211	if mode=='wrap': neigh = np.mod(neigh,bound); inside=True
212	else: inside = np.logical_and(np.all(neigh>=0,axis=0),np.all(neigh<bound,axis=0))
213
214	neighIndex = np.ravel_multi_index(neigh, shape, mode=mode)
215	return neighIndex, inside
216
217def TakeAtOffset(u,offset, padding=np.nan, **kwargs):
218	"""
219	- padding: outside fill value. Set padding=None for periodic boundary conditions
220	- **kwargs : passed to OffsetToIndex
221	"""
222	mode = 'wrap' if padding is None else 'clip'
223	neighIndex, inside = OffsetToIndex(u.shape,offset,mode=mode, **kwargs)
224
225	values = u.reshape(-1)[neighIndex]
226	if padding is not None:
227		if ad.isndarray(values):
228			values[np.logical_not(inside)] = padding
229		elif not inside:
230			values = padding
231	return values
232
233def AlignedSum(u,offset,multiples,weights,**kwargs):
234	"""
235	Returns sum along the direction offset, with specified multiples and weights.
236	Input : 
237	 - kwargs : passed to TakeAtOffset
238	"""
239	return sum(TakeAtOffset(u,mult*ad.asarray(offset),**kwargs)*weight 
240		for mult,weight in zip(multiples,weights))
241
242
243# --------- Degenerate elliptic finite differences -------
244
245def Diff2(u,offset,gridScale=1.,order=2,**kwargs):
246	"""
247	Approximates <offset, (d^2 u) offset> with specified accuracy order.
248	When order=2, corresponds to the (negated) degenerate elliptic scheme
249	$$
250		<e,(d^2 u) e> = (u(x+h*e)-2*u(x)+u(x-h*e))/h^2 + O(h^2),
251	$$
252	where e = offset and h=gridScale.
253	Input : 
254	 - u : array of shape (n1,...,nd) where n1,...,nd are the domain dimensions
255	 - offset : array with integer entries, of shape (d,k1,...,kr,n1,...,nd) or (d,k1,...,kr)
256	   where d is the number of dimensions in the domain, and k1,...,kr are arbitrary.
257	 - gridscale (optional) : scale of the discretization grid used in the finite differences
258	 - order (optional) : approximation order of the finite differences
259	 - kwargs : passed to AlignedSum
260	"""
261	if   order<=2: multiples,weights = (1,0,-1),(1.,-2.,1.)
262	elif order<=4: multiples,weights = (2,1,0,-1,-2),(-1/12.,4/3.,-15/6.,4/3.,-1/12.)
263	else: raise ValueError("Unsupported order") 
264	return AlignedSum(u,offset,multiples,np.array(weights)/gridScale**2,**kwargs)
265
266
267def DiffUpwind(u,offset,gridScale=1.,order=1,**kwargs):
268	"""
269	Approximates <grad u, offset> with specified accuracy order.
270	When order=1, corresponds the (negated) degenerate elliptic scheme
271	$$
272		 <grad u, e> = (u(x+h*e)-u(x))/h + O(h)
273	$$
274	where e = offset and h=gridScale.
275	Input : 
276	 - u : array of shape (n1,...,nd) where n1,...,nd are the domain dimensions
277	 - offset : array with integer entries, of shape (d,k1,...,kr,n1,...,nd) or (d,k1,...,kr)
278	   where d is the number of dimensions in the domain, and k1,...,kr are arbitrary.
279	 - gridscale (optional) : scale of the discretization grid used in the finite differences
280	 - order (optional) : approximation order of the finite differences
281	 - kwargs : passed to AlignedSum
282	"""
283	if   order==1: multiples,weights = (1,0),	( 1.,-1.)
284	elif order==2: multiples,weights = (2,1,0),  (-0.5,2.,-1.5)
285	elif order==3: multiples,weights = (3,2,1,0),(1./3.,-1.5,3.,-11./6.)
286	else: raise ValueError("Unsupported order")
287	return AlignedSum(u,offset,multiples,np.array(weights)/gridScale,**kwargs)
288
289# --------- Non-Degenerate elliptic finite differences ---------
290
291def DiffCentered(u,offset,gridScale=1.,order=2,**kwargs):
292	"""
293	Approximates <d u, offset> with specified accuracy order.
294	When order=2, corresponds to the centered finite difference
295	$$
296		 <grad u, e> = (u(x+h*e)-u(x-h*e))/(2h) + O(h^2)
297	$$
298	where e = offset and h=gridScale.
299		Input : 
300	 - u : array of shape (n1,...,nd) where n1,...,nd are the domain dimensions
301	 - offset : array with integer entries, of shape (d,k1,...,kr,n1,...,nd) or (d,k1,...,kr)
302	   where d is the number of dimensions in the domain, and k1,...,kr are arbitrary.
303	 - gridscale (optional) : scale of the discretization grid used in the finite differences
304	 - order (optional) : approximation order of the finite differences
305	 - kwargs : passed to AlignedSum
306	"""
307	if   order<=2: multiples,weights = ( 1,-1),( 1.,-1.)
308	elif order<=4: multiples,weights = ( 2, 1,-1,-2),(-1/6., 4/3.,-4/3., 1/6.)
309	else: raise ValueError("Unsupported order")
310	return AlignedSum(u,offset,multiples,np.array(weights)/(2*gridScale),**kwargs)
311
312def DiffCross(u,offset0,offset1,gridScale=1.,order=2,**kwargs):
313	"""
314	Approximates <offsets0, (d^2 u) offset1> with second order accuracy.
315	Centered finite differences scheme, but lacking the degenerate ellipticity property.
316	"""
317	if   order<=2: multiples,weights = ( 1,-1),(1.,1.)
318	elif order<=4: multiples,weights = ( 2, 1,-1,-2),(-1/12.,4/3.,4/3.,-1/12.)
319	else: raise ValueError("Unsupported order")
320	weights = np.array(weights)/(4*gridScale**2)
321	return (AlignedSum(u,offset0+offset1,multiples,weights,**kwargs) 
322		- AlignedSum(u,offset0-offset1,multiples,weights,**kwargs) )
323
324# ------------ Composite finite differences ----------
325
326def AxesOffsets(u=None,offsets=None,dimension=None):
327	"""
328	Returns the offsets corresponding to the axes.
329		Inputs : 
330	 - offsets (optional). Defaults to np.eye(dimension)
331	 - dimension (optional). Defaults to u.ndim
332	"""
333	if offsets is None:
334		if dimension is None:
335			dimension = u.ndim
336		offsets = np.eye(dimension).astype(int)
337	return offsets
338
339
340
341def DiffHessian(u,offsets=None,dimension=None,**kwargs):
342	"""
343	Approximates the matrix (<offsets[i], (d^2 u) offsets[j]> )_{ij}, using AxesOffsets as offsets.
344	Centered and cross finite differences are used, thus lacking the degenerate ellipticity property.
345	"""
346	from . import Metrics
347	offsets=AxesOffsets(u,offsets,dimension)
348	return Metrics.misc.expand_symmetric_matrix([
349		Diff2(u,offsets[i],**kwargs) if i==j else DiffCross(u,offsets[i],offsets[j],**kwargs) 
350		for i in range(len(offsets)) for j in range(i+1)])
351
352def DiffGradient(u,offsets=None,dimension=None,**kwargs):
353	"""
354	Approximates the vector (<d u, offsets[i]>)_i, using AxesOffsets as offsets
355	Centered finite differences are used, thus lacking the degerate ellipticity property.
356	"""
357	return DiffCentered(u,AxesOffsets(u,offsets,dimension),**kwargs)
358
359def DiffEll(u,offset,gridScale=1.0,order=2,α=0.,**kwargs):
360	"""
361	Helper function for discretizing (<grad u,e> - α)^2, which appears in elliptic energies. Returns 
362	$$
363	    [u(x)-u(x-h*e)-αh, u(x+h*e)-u(x)-αh]/(h sqrt(2))
364	$$
365	if order=2. Sum the squares to approximate (<grad u,e>-α)^2. Also accepts order=4.
366	"""
367	assert order in (2,4); s2 = np.sqrt(2); s6 = np.sqrt(6)
368	if order==2: return ad.array([(-DiffUpwind(u,-offset,gridScale,**kwargs)-α)/s2,
369	                              ( DiffUpwind(u, offset,gridScale,**kwargs)-α)/s2])
370	if order==4: return ad.array([(-DiffUpwind(u,-offset,gridScale,order=2,**kwargs)-α)/s6,
371	                              (DiffCentered(u,offset,gridScale,        **kwargs)-α)*(2/s6),
372                                  ( DiffUpwind(u, offset,gridScale,order=2,**kwargs)-α)/s6])
373
374# ----------- Interpolation ---------
375
376def UniformGridInterpolator1D(bounds,values,mode='clip',axis=-1):
377	"""
378	Interpolation on a uniform grid. mode is in ('clip','wrap', ('fill',fill_value) )
379	"""
380	val = values.swapaxes(axis,0)
381	fill_value = None
382	if isinstance(mode,tuple):
383		mode,fill_value = mode		
384	def interp(position):
385		endpoint=not (mode=='wrap')
386		size = val.size
387		index_continuous = (size-int(endpoint))*(position-bounds[0])/(bounds[-1]-bounds[0])
388		index0 = np.floor(index_continuous).astype(int)
389		index1 = np.ceil(index_continuous).astype(int)
390		index_rem = index_continuous-index0
391		
392		fill_indices=False
393		if mode=='wrap':
394			index0=index0%size
395			index1=index1%size
396		else: 
397			if mode=='fill':
398				 fill_indices = np.logical_or(index0<0, index1>=size)
399			index0 = np.clip(index0,0,size-1) 
400			index1 = np.clip(index1,0,size-1)
401		
402		index_rem = index_rem.reshape(index_rem.shape+(1,)*(val.ndim-1))
403		result = val[index0]*(1.-index_rem) + val[index1]*index_rem
404		if mode=='fill': result[fill_indices] = fill_value
405		result = np.moveaxis(result,range(position.ndim),range(-position.ndim,0))
406		return result
407	return interp
408
409# def AxesOrderingBounds(grid):
410# 	dim = len(grid)
411# 	lbounds = grid.__getitem__((slice(None),)+(0,)*dim)
412# 	ubounds = grid.__getitem__((slice(None),)+(-1,)*dim)
413
414# 	def active(i):
415# 		di = grid.__getitem__((slice(None),)+(0,)*i+(1,)+(0,)*(dim-1-i))
416# 		return np.argmax(np.abs(di-lbounds))
417# 	axes = tuple(active(i) for i in range(dim))
418
419# 	return axes,lbounds,ubounds 
def as_field(u, shape, conditional=True, depth=None):
33def as_field(u,shape,conditional=True,depth=None):
34	"""
35	Checks if the last dimensions of u match the given shape. 
36	If not, u is extended with these additional dimensions.
37	conditional : if False, reshaping is always done
38	depth (optional) : the depth of the geometrical tensor field (1: vectors, 2: matrices)
39	"""
40	u=ad.asarray(u)
41	ndim = len(shape)
42	def as_is():
43		if not conditional: return False
44		elif depth is None: return u.ndim>=ndim and u.shape[-ndim:]==shape
45		else: assert u.shape[depth:] in (tuple(),shape); return u.shape[depth:]==shape
46	if as_is(): return u
47	else: return np.broadcast_to(u.reshape(u.shape+(1,)*ndim), u.shape+shape)

Checks if the last dimensions of u match the given shape. If not, u is extended with these additional dimensions. conditional : if False, reshaping is always done depth (optional) : the depth of the geometrical tensor field (1: vectors, 2: matrices)

def common_shape(arrays, depths):
49def common_shape(arrays,depths):
50	"""Finds a common shape to the arrays for broadcasting"""
51	arrays = tuple(None if arr is None else ad.asarray(arr) for arr in arrays)
52	assert len(arrays)==len(depths)
53	for arr,depth in zip(arrays,depths):
54		if arr is None: continue
55		shape = arr.shape[depth:]
56		if shape!=tuple(): break
57	for arr,depth in zip(arrays,depths): 
58		assert arr is None or arr.shape[depth:] in (tuple(),shape)
59	return shape

Finds a common shape to the arrays for broadcasting

def common_field(arrays, depths, shape=None):
61def common_field(arrays,depths,shape=None):
62	"""
63	Adds trailing dimensions, and broadcasts the given arrays, for suitable interoperation.
64	
65	Inputs: 
66	- arrays : a list [a_1,...,a_n], or iterable, of numeric arrays such that
67	 a_i.shape = shape_i + shape, or a_i.shape = shape_i, for each 1<=i<=n.
68	- depths : defined as [len(shape_i) for 1<=i<=n]
69	- shape (optional) : the trailing shape.
70	
71	Output:
72	- the arrays, with added trailing and dimensions broadcasting so that
73	 a_i.shape = shape_i + shape for each 1<=i<=n.
74
75	"""
76	arrays = tuple(None if arr is None else ad.asarray(arr) for arr in arrays)
77	assert len(arrays)==len(depths)
78	if shape is None: shape = common_shape(arrays,depths)
79	return tuple(None if arr is None else as_field(arr,shape,depth=depth) 
80		for (arr,depth) in zip(arrays,depths))

Adds trailing dimensions, and broadcasts the given arrays, for suitable interoperation.

Inputs:

  • arrays : a list [a_1,...,a_n], or iterable, of numeric arrays such that a_i.shape = shape_i + shape, or a_i.shape = shape_i, for each 1<=i<=n.
  • depths : defined as [len(shape_i) for 1<=i<=n]
  • shape (optional) : the trailing shape.

Output:

  • the arrays, with added trailing and dimensions broadcasting so that a_i.shape = shape_i + shape for each 1<=i<=n.
def round_up_ratio(num, den):
82def round_up_ratio(num,den):
83	"""
84	Returns the least multiple of den after num.
85	num and den must be integers, with den>0. 
86	"""
87	num,den = np.asarray(num),np.asarray(den)
88	return (num+den-1)//den

Returns the least multiple of den after num. num and den must be integers, with den>0.

def block_expand(arr, shape_i, shape_o=None, **kwargs):
 90def block_expand(arr,shape_i,shape_o=None,**kwargs):
 91	"""
 92	Reshape an array so as to factor shape_i (the inner shape),
 93	and move these axes last.
 94	Inputs : 
 95	 - **kwargs : passed to np.pad
 96	Output : 
 97	 - padded and reshaped array
 98	"""
 99	ndim = len(shape_i)
100	if ndim==0: return arr # Empty block
101	shape_pre = arr.shape[:-ndim]
102	ndim_pre = len(shape_pre)
103	shape_tot=np.array(arr.shape[-ndim:])
104	shape_i = np.array(shape_i)
105
106	# Extend data
107	if shape_o is None: shape_o = round_up_ratio(shape_tot,shape_i)
108	shape_pad = (0,)*ndim_pre + tuple(shape_o*shape_i - shape_tot)
109	arr = np.pad(arr, tuple( (0,s) for s in shape_pad), **kwargs) 
110
111	# Reshape
112	shape_interleaved = tuple(np.stack( (shape_o,shape_i), axis=1).flatten())
113	arr = arr.reshape(shape_pre + shape_interleaved)
114
115	# Move axes
116	rg = np.arange(ndim)
117	axes_interleaved = ndim_pre + 1+2*rg
118	axes_split = ndim_pre + ndim+rg
119	arr = np.moveaxis(arr,axes_interleaved,axes_split)
120
121	return arr

Reshape an array so as to factor shape_i (the inner shape), and move these axes last. Inputs :

  • **kwargs : passed to np.pad Output :
  • padded and reshaped array
def block_squeeze(arr, shape):
123def block_squeeze(arr,shape):
124	"""
125	Inverse operation to block_expand.
126	"""
127	ndim = len(shape)
128	if ndim==0: return arr # Empty block
129	shape_pre = arr.shape[:-2*ndim]
130	ndim_pre = len(shape_pre)
131	shape_o = arr.shape[(-2*ndim):-ndim]
132	shape_i = arr.shape[-ndim:]
133
134	# Move axes
135	rg = np.arange(ndim)
136	axes_interleaved = ndim_pre + 1+2*rg
137	axes_split = ndim_pre + ndim+rg
138	arr = np.moveaxis(arr,axes_split,axes_interleaved)
139
140	# Reshape
141	arr = arr.reshape(shape_pre
142		+tuple(s_o*s_i for (s_o,s_i) in zip(shape_o,shape_i)) )
143
144	# Extract subdomain
145	region = tuple(slice(0,s) for s in (shape_pre+shape))
146	arr = arr.__getitem__(region)
147
148	return arr

Inverse operation to block_expand.

def block_neighbors(shape_i, top=True):
150def block_neighbors(shape_i,top=True):
151	"""
152	The first layer of neighbors to a block, along the top or bottom, ordered 
153	by increasing indices. Used for efficient data load when a implementing 
154	gradients, divergences, etc, with a compact scheme
155	"""
156	shape_i = np.array(shape_i); vdim=len(shape_i); size_i = np.prod(shape_i)
157	aX = [np.arange(s+1) if top else np.arange(s-1,2*s) for s in shape_i]
158	x_e = np.array(np.meshgrid(*aX)).astype(int)
159	x_e = np.moveaxis(x_e.reshape((vdim,-1)),0,-1)
160	x_e = np.array([x for x in x_e if np.any(x==(shape_i if top else shape_i-1))])
161	ind = np.arange(2**vdim*size_i).reshape((2,)*vdim+tuple(shape_i))
162	ind = block_squeeze(ind,tuple(2*shape_i))
163	ind_x = np.array([ind[tuple(x)] for x in x_e])
164	x_e = x_e[np.argsort(ind_x)]
165	#print(np.array([ind[tuple(x)] for x in x_e]))
166	if not top: x_e-=shape_i
167	return x_e

The first layer of neighbors to a block, along the top or bottom, ordered by increasing indices. Used for efficient data load when a implementing gradients, divergences, etc, with a compact scheme

def BoundedSlices(slices, shape):
172def BoundedSlices(slices,shape):
173	"""
174	Returns the input slices with None replaced with the upper bound
175	from the given shape
176	"""
177	if slices[-1]==Ellipsis:
178		slices=slices[:-1]+(slice(None,None,None),)*(len(shape)-len(slices)+1)
179	def BoundedSlice(s,n):
180		if not isinstance(s,slice):
181			return slice(s,s+1)
182		else:
183			return slice(s.start, n if s.stop is None else s.stop, s.step)
184	return tuple(BoundedSlice(s,n) for s,n in zip(slices,shape))

Returns the input slices with None replaced with the upper bound from the given shape

def OffsetToIndex(shape, offset, mode='clip', uniform=None, where=(Ellipsis,)):
186def OffsetToIndex(shape,offset, mode='clip', uniform=None, where=(Ellipsis,)):
187	"""
188	Returns the index corresponding to position + offset, 
189	and a boolean for wether it falls inside the domain.
190	"""
191	ndim = len(shape) # Domain dimension
192	assert(offset.shape[0]==ndim)
193	if uniform is None: # Uniform = True iff offsets are independent of the position in the domain
194		uniform = not ((offset.ndim > ndim) and (offset.shape[-ndim:]==shape))
195
196	odim = (offset.ndim-1) if uniform else (offset.ndim - ndim-1) # Dimensions for distinct offsets 
197	everywhere = where==(Ellipsis,)
198	xp=ad.cupy_generic.get_array_module(offset)
199
200	grid = (xp.mgrid[tuple(slice(n) for n in shape)]
201		if everywhere else
202		np.squeeze(xp.mgrid[BoundedSlices(where,shape)],
203		tuple(1+i for i,s in enumerate(where) if not isinstance(s,slice)) ) )
204	grid = grid.reshape( (ndim,) + (1,)*odim+grid.shape[1:])
205
206	if not everywhere and not uniform:
207		offset = offset[(slice(None),)*(1+odim)+where]
208
209	neigh = grid + (offset.reshape(offset.shape + (1,)*ndim) if uniform else offset)
210	bound = xp.array(shape,dtype=neigh.dtype).reshape((ndim,)+(1,)*(neigh.ndim-1))
211
212	if mode=='wrap': neigh = np.mod(neigh,bound); inside=True
213	else: inside = np.logical_and(np.all(neigh>=0,axis=0),np.all(neigh<bound,axis=0))
214
215	neighIndex = np.ravel_multi_index(neigh, shape, mode=mode)
216	return neighIndex, inside

Returns the index corresponding to position + offset, and a boolean for wether it falls inside the domain.

def TakeAtOffset(u, offset, padding=nan, **kwargs):
218def TakeAtOffset(u,offset, padding=np.nan, **kwargs):
219	"""
220	- padding: outside fill value. Set padding=None for periodic boundary conditions
221	- **kwargs : passed to OffsetToIndex
222	"""
223	mode = 'wrap' if padding is None else 'clip'
224	neighIndex, inside = OffsetToIndex(u.shape,offset,mode=mode, **kwargs)
225
226	values = u.reshape(-1)[neighIndex]
227	if padding is not None:
228		if ad.isndarray(values):
229			values[np.logical_not(inside)] = padding
230		elif not inside:
231			values = padding
232	return values
  • padding: outside fill value. Set padding=None for periodic boundary conditions
  • **kwargs : passed to OffsetToIndex
def AlignedSum(u, offset, multiples, weights, **kwargs):
234def AlignedSum(u,offset,multiples,weights,**kwargs):
235	"""
236	Returns sum along the direction offset, with specified multiples and weights.
237	Input : 
238	 - kwargs : passed to TakeAtOffset
239	"""
240	return sum(TakeAtOffset(u,mult*ad.asarray(offset),**kwargs)*weight 
241		for mult,weight in zip(multiples,weights))

Returns sum along the direction offset, with specified multiples and weights. Input :

  • kwargs : passed to TakeAtOffset
def Diff2(u, offset, gridScale=1.0, order=2, **kwargs):
246def Diff2(u,offset,gridScale=1.,order=2,**kwargs):
247	"""
248	Approximates <offset, (d^2 u) offset> with specified accuracy order.
249	When order=2, corresponds to the (negated) degenerate elliptic scheme
250	$$
251		<e,(d^2 u) e> = (u(x+h*e)-2*u(x)+u(x-h*e))/h^2 + O(h^2),
252	$$
253	where e = offset and h=gridScale.
254	Input : 
255	 - u : array of shape (n1,...,nd) where n1,...,nd are the domain dimensions
256	 - offset : array with integer entries, of shape (d,k1,...,kr,n1,...,nd) or (d,k1,...,kr)
257	   where d is the number of dimensions in the domain, and k1,...,kr are arbitrary.
258	 - gridscale (optional) : scale of the discretization grid used in the finite differences
259	 - order (optional) : approximation order of the finite differences
260	 - kwargs : passed to AlignedSum
261	"""
262	if   order<=2: multiples,weights = (1,0,-1),(1.,-2.,1.)
263	elif order<=4: multiples,weights = (2,1,0,-1,-2),(-1/12.,4/3.,-15/6.,4/3.,-1/12.)
264	else: raise ValueError("Unsupported order") 
265	return AlignedSum(u,offset,multiples,np.array(weights)/gridScale**2,**kwargs)

Approximates with specified accuracy order. When order=2, corresponds to the (negated) degenerate elliptic scheme $$ = (u(x+he)-2u(x)+u(x-h*e))/h^2 + O(h^2), $$ where e = offset and h=gridScale. Input :

  • u : array of shape (n1,...,nd) where n1,...,nd are the domain dimensions
  • offset : array with integer entries, of shape (d,k1,...,kr,n1,...,nd) or (d,k1,...,kr) where d is the number of dimensions in the domain, and k1,...,kr are arbitrary.
  • gridscale (optional) : scale of the discretization grid used in the finite differences
  • order (optional) : approximation order of the finite differences
  • kwargs : passed to AlignedSum
def DiffUpwind(u, offset, gridScale=1.0, order=1, **kwargs):
268def DiffUpwind(u,offset,gridScale=1.,order=1,**kwargs):
269	"""
270	Approximates <grad u, offset> with specified accuracy order.
271	When order=1, corresponds the (negated) degenerate elliptic scheme
272	$$
273		 <grad u, e> = (u(x+h*e)-u(x))/h + O(h)
274	$$
275	where e = offset and h=gridScale.
276	Input : 
277	 - u : array of shape (n1,...,nd) where n1,...,nd are the domain dimensions
278	 - offset : array with integer entries, of shape (d,k1,...,kr,n1,...,nd) or (d,k1,...,kr)
279	   where d is the number of dimensions in the domain, and k1,...,kr are arbitrary.
280	 - gridscale (optional) : scale of the discretization grid used in the finite differences
281	 - order (optional) : approximation order of the finite differences
282	 - kwargs : passed to AlignedSum
283	"""
284	if   order==1: multiples,weights = (1,0),	( 1.,-1.)
285	elif order==2: multiples,weights = (2,1,0),  (-0.5,2.,-1.5)
286	elif order==3: multiples,weights = (3,2,1,0),(1./3.,-1.5,3.,-11./6.)
287	else: raise ValueError("Unsupported order")
288	return AlignedSum(u,offset,multiples,np.array(weights)/gridScale,**kwargs)

Approximates with specified accuracy order. When order=1, corresponds the (negated) degenerate elliptic scheme $$ = (u(x+h*e)-u(x))/h + O(h) $$ where e = offset and h=gridScale. Input :

  • u : array of shape (n1,...,nd) where n1,...,nd are the domain dimensions
  • offset : array with integer entries, of shape (d,k1,...,kr,n1,...,nd) or (d,k1,...,kr) where d is the number of dimensions in the domain, and k1,...,kr are arbitrary.
  • gridscale (optional) : scale of the discretization grid used in the finite differences
  • order (optional) : approximation order of the finite differences
  • kwargs : passed to AlignedSum
def DiffCentered(u, offset, gridScale=1.0, order=2, **kwargs):
292def DiffCentered(u,offset,gridScale=1.,order=2,**kwargs):
293	"""
294	Approximates <d u, offset> with specified accuracy order.
295	When order=2, corresponds to the centered finite difference
296	$$
297		 <grad u, e> = (u(x+h*e)-u(x-h*e))/(2h) + O(h^2)
298	$$
299	where e = offset and h=gridScale.
300		Input : 
301	 - u : array of shape (n1,...,nd) where n1,...,nd are the domain dimensions
302	 - offset : array with integer entries, of shape (d,k1,...,kr,n1,...,nd) or (d,k1,...,kr)
303	   where d is the number of dimensions in the domain, and k1,...,kr are arbitrary.
304	 - gridscale (optional) : scale of the discretization grid used in the finite differences
305	 - order (optional) : approximation order of the finite differences
306	 - kwargs : passed to AlignedSum
307	"""
308	if   order<=2: multiples,weights = ( 1,-1),( 1.,-1.)
309	elif order<=4: multiples,weights = ( 2, 1,-1,-2),(-1/6., 4/3.,-4/3., 1/6.)
310	else: raise ValueError("Unsupported order")
311	return AlignedSum(u,offset,multiples,np.array(weights)/(2*gridScale),**kwargs)

Approximates with specified accuracy order. When order=2, corresponds to the centered finite difference $$ = (u(x+he)-u(x-he))/(2h) + O(h^2) $$ where e = offset and h=gridScale. Input :

  • u : array of shape (n1,...,nd) where n1,...,nd are the domain dimensions
  • offset : array with integer entries, of shape (d,k1,...,kr,n1,...,nd) or (d,k1,...,kr) where d is the number of dimensions in the domain, and k1,...,kr are arbitrary.
  • gridscale (optional) : scale of the discretization grid used in the finite differences
  • order (optional) : approximation order of the finite differences
  • kwargs : passed to AlignedSum
def DiffCross(u, offset0, offset1, gridScale=1.0, order=2, **kwargs):
313def DiffCross(u,offset0,offset1,gridScale=1.,order=2,**kwargs):
314	"""
315	Approximates <offsets0, (d^2 u) offset1> with second order accuracy.
316	Centered finite differences scheme, but lacking the degenerate ellipticity property.
317	"""
318	if   order<=2: multiples,weights = ( 1,-1),(1.,1.)
319	elif order<=4: multiples,weights = ( 2, 1,-1,-2),(-1/12.,4/3.,4/3.,-1/12.)
320	else: raise ValueError("Unsupported order")
321	weights = np.array(weights)/(4*gridScale**2)
322	return (AlignedSum(u,offset0+offset1,multiples,weights,**kwargs) 
323		- AlignedSum(u,offset0-offset1,multiples,weights,**kwargs) )

Approximates with second order accuracy. Centered finite differences scheme, but lacking the degenerate ellipticity property.

def AxesOffsets(u=None, offsets=None, dimension=None):
327def AxesOffsets(u=None,offsets=None,dimension=None):
328	"""
329	Returns the offsets corresponding to the axes.
330		Inputs : 
331	 - offsets (optional). Defaults to np.eye(dimension)
332	 - dimension (optional). Defaults to u.ndim
333	"""
334	if offsets is None:
335		if dimension is None:
336			dimension = u.ndim
337		offsets = np.eye(dimension).astype(int)
338	return offsets

Returns the offsets corresponding to the axes. Inputs :

  • offsets (optional). Defaults to np.eye(dimension)
  • dimension (optional). Defaults to u.ndim
def DiffHessian(u, offsets=None, dimension=None, **kwargs):
342def DiffHessian(u,offsets=None,dimension=None,**kwargs):
343	"""
344	Approximates the matrix (<offsets[i], (d^2 u) offsets[j]> )_{ij}, using AxesOffsets as offsets.
345	Centered and cross finite differences are used, thus lacking the degenerate ellipticity property.
346	"""
347	from . import Metrics
348	offsets=AxesOffsets(u,offsets,dimension)
349	return Metrics.misc.expand_symmetric_matrix([
350		Diff2(u,offsets[i],**kwargs) if i==j else DiffCross(u,offsets[i],offsets[j],**kwargs) 
351		for i in range(len(offsets)) for j in range(i+1)])

Approximates the matrix (

def DiffGradient(u, offsets=None, dimension=None, **kwargs):
353def DiffGradient(u,offsets=None,dimension=None,**kwargs):
354	"""
355	Approximates the vector (<d u, offsets[i]>)_i, using AxesOffsets as offsets
356	Centered finite differences are used, thus lacking the degerate ellipticity property.
357	"""
358	return DiffCentered(u,AxesOffsets(u,offsets,dimension),**kwargs)

Approximates the vector (

def DiffEll(u, offset, gridScale=1.0, order=2, α=0.0, **kwargs):
360def DiffEll(u,offset,gridScale=1.0,order=2,α=0.,**kwargs):
361	"""
362	Helper function for discretizing (<grad u,e> - α)^2, which appears in elliptic energies. Returns 
363	$$
364	    [u(x)-u(x-h*e)-αh, u(x+h*e)-u(x)-αh]/(h sqrt(2))
365	$$
366	if order=2. Sum the squares to approximate (<grad u,e>-α)^2. Also accepts order=4.
367	"""
368	assert order in (2,4); s2 = np.sqrt(2); s6 = np.sqrt(6)
369	if order==2: return ad.array([(-DiffUpwind(u,-offset,gridScale,**kwargs)-α)/s2,
370	                              ( DiffUpwind(u, offset,gridScale,**kwargs)-α)/s2])
371	if order==4: return ad.array([(-DiffUpwind(u,-offset,gridScale,order=2,**kwargs)-α)/s6,
372	                              (DiffCentered(u,offset,gridScale,        **kwargs)-α)*(2/s6),
373                                  ( DiffUpwind(u, offset,gridScale,order=2,**kwargs)-α)/s6])

Helper function for discretizing ( - α)^2, which appears in elliptic energies. Returns $$ [u(x)-u(x-he)-αh, u(x+he)-u(x)-αh]/(h sqrt(2)) $$ if order=2. Sum the squares to approximate (-α)^2. Also accepts order=4.

def UniformGridInterpolator1D(bounds, values, mode='clip', axis=-1):
377def UniformGridInterpolator1D(bounds,values,mode='clip',axis=-1):
378	"""
379	Interpolation on a uniform grid. mode is in ('clip','wrap', ('fill',fill_value) )
380	"""
381	val = values.swapaxes(axis,0)
382	fill_value = None
383	if isinstance(mode,tuple):
384		mode,fill_value = mode		
385	def interp(position):
386		endpoint=not (mode=='wrap')
387		size = val.size
388		index_continuous = (size-int(endpoint))*(position-bounds[0])/(bounds[-1]-bounds[0])
389		index0 = np.floor(index_continuous).astype(int)
390		index1 = np.ceil(index_continuous).astype(int)
391		index_rem = index_continuous-index0
392		
393		fill_indices=False
394		if mode=='wrap':
395			index0=index0%size
396			index1=index1%size
397		else: 
398			if mode=='fill':
399				 fill_indices = np.logical_or(index0<0, index1>=size)
400			index0 = np.clip(index0,0,size-1) 
401			index1 = np.clip(index1,0,size-1)
402		
403		index_rem = index_rem.reshape(index_rem.shape+(1,)*(val.ndim-1))
404		result = val[index0]*(1.-index_rem) + val[index1]*index_rem
405		if mode=='fill': result[fill_indices] = fill_value
406		result = np.moveaxis(result,range(position.ndim),range(-position.ndim,0))
407		return result
408	return interp

Interpolation on a uniform grid. mode is in ('clip','wrap', ('fill',fill_value) )