agd.AutomaticDifferentiation.Sparse

  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
  4import numpy as np
  5import functools
  6from . import functional
  7from . import Base
  8from . import cupy_support as cps
  9from . import ad_generic
 10from . import cupy_generic
 11from . import misc
 12from . import Dense
 13
 14_add_dim = misc._add_dim; _pad_last = misc._pad_last; _concatenate=misc._concatenate;
 15
 16class spAD(Base.baseAD):
 17	"""
 18	A class for sparse forward automatic differentiation
 19	"""
 20
 21	def __init__(self,value,coef=None,index=None,broadcast_ad=False):
 22		if self.is_ad(value): # Case where one should just reproduce value
 23			assert coef is None and index is None
 24			self.value,self.coef,self.index = value.value,value.coef,value.index
 25			return
 26		if ad_generic.is_ad(value):
 27			raise Base.ADCastError(f"Attempting to cast {type(value)} to incompatible type spAD")
 28			
 29		assert ((coef is None) and (index is None)) or (coef.shape==index.shape)
 30
 31		self.value = ad_generic.asarray(value)
 32		shape =self.shape
 33		shape2=shape+(0,)
 34		self.coef  = (np.zeros_like(value,shape=shape2) if coef is None 
 35			else misc._test_or_broadcast_ad(coef,shape,broadcast_ad) ) 
 36		self.index = (np.zeros_like(value,shape=shape2,
 37			dtype=cupy_generic.samesize_int_t(value.dtype))  
 38			if index is None else misc._test_or_broadcast_ad(index,shape,broadcast_ad) )
 39
 40	@classmethod
 41	def order(cls): return 1
 42	def copy(self,order='C'):
 43		return self.new(self.value.copy(order=order),
 44			self.coef.copy(order=order),self.index.copy(order=order))
 45	def as_tuple(self): return self.value,self.coef,self.index
 46
 47	def __copy__(self): return self.copy(order='K')
 48	def __deepcopy__(self,*args): 
 49		return self.new(self.value.__deepcopy__(*args),
 50			self.coef.__deepcopy__(*args),self.index.__deepcopy__(*args))
 51
 52	# Representation 
 53	def __iter__(self):
 54		for value,coef,index in zip(self.value,self.coef,self.index):
 55			yield self.new(value,coef,index)
 56
 57	def __str__(self):
 58		return "spAD"+str((self.value,self.coef,self.index))
 59	def __repr__(self):
 60		return "spAD"+repr((self.value,self.coef,self.index))	
 61
 62	# Operators
 63	def as_func(self,h=None):
 64		"""Replaces the symbolic perturbation with h, if specified."""
 65		if h is None: 
 66			lin = self.tangent_operator()
 67			return lambda h : (lin*h).reshape(self.shape) + misc.add_ndim(self.value,h.ndim-1)
 68		value,coef = (misc.add_ndim(e,h.ndim-1) for e in (self.value,self.coef))
 69		return value+(coef*h[self.index]).sum(axis=self.ndim)
 70
 71	def __add__(self,other):
 72		if self.is_ad(other):
 73			value = self.value+other.value
 74			return self.new(value, _concatenate(self.coef,other.coef,value.shape), 
 75				_concatenate(self.index,other.index,value.shape))
 76		else:
 77			return self.new(self.value+other, self.coef, self.index, broadcast_ad=True)
 78
 79	def __sub__(self,other):
 80		if self.is_ad(other):
 81			value = self.value-other.value
 82			return self.new(self.value-other.value, _concatenate(self.coef,-other.coef,value.shape), _concatenate(self.index,other.index,value.shape))
 83		else:
 84			return self.new(self.value-other, self.coef, self.index, broadcast_ad=True)
 85
 86	def __mul__(self,other):
 87		if self.is_ad(other):
 88			value = self.value*other.value
 89			coef1,coef2 = _add_dim(other.value)*self.coef,_add_dim(self.value)*other.coef
 90			index1,index2 = np.broadcast_to(self.index,coef1.shape),np.broadcast_to(other.index,coef2.shape)
 91			return self.new(value,_concatenate(coef1,coef2),_concatenate(index1,index2))
 92		elif self.isndarray(other):
 93			value = self.value*other
 94			coef = _add_dim(other)*self.coef
 95			index = np.broadcast_to(self.index,coef.shape)
 96			return self.new(value,coef,index)
 97		else:
 98			return self.new(self.value*other,other*self.coef,self.index)
 99
100	def __truediv__(self,other):
101		if self.is_ad(other):
102			return self.new(self.value/other.value,
103				_concatenate(self.coef*_add_dim(1/other.value),other.coef*_add_dim(-self.value/other.value**2)),
104				_concatenate(self.index,other.index))
105		elif self.isndarray(other):
106			return self.new(self.value/other,self.coef*_add_dim(1./other),self.index)
107		else:
108			return self.new(self.value/other,self.coef/other,self.index)
109
110	__rmul__ = __mul__
111	__radd__ = __add__
112	def __rsub__(self,other): 		return -(self-other)
113	def __rtruediv__(self,other): 	
114		value = other/self.value
115		coef = self.coef*_add_dim(-other/self.value**2)
116		index = np.broadcast_to(self.index,coef.shape)
117		return self.new(value,coef,index)
118
119	def __neg__(self):		return self.new(-self.value,-self.coef,self.index)
120
121	# Math functions
122	def _math_helper(self,deriv):
123		a,b=deriv
124		return self.new(a,_add_dim(b)*self.coef,self.index)
125
126	@classmethod
127	def compose(cls,a,t):
128		assert isinstance(a,Dense.denseAD) and all(cls.is_ad(b) for b in t)
129		lens = tuple(len(b) for b in t)
130		assert a.size_ad == sum(lens)
131		t = tuple(np.moveaxis(b,0,-1) for b in t)
132		a_coefs = np.split(a.coef,np.cumsum(lens[:-1]),axis=-1)
133		def FlattenLast2(arr): return arr.reshape(arr.shape[:-2]+(np.prod(arr.shape[-2:],dtype=int),))
134		coef = tuple(_add_dim(c)*b.coef for c,b in zip(a_coefs,t) )
135		coef = np.concatenate( tuple(FlattenLast2(c) for c in coef), axis=-1)
136		index = np.broadcast_to(np.concatenate( tuple(FlattenLast2(b.index) 
137			for b in t), axis=-1),coef.shape)
138		return cls.new(a.value,coef,index)
139
140	#Indexing
141	@property
142	def size_ad(self):  return self.coef.shape[-1]
143
144	def __getitem__(self,key):
145		ekey = misc.key_expand(key)
146		return self.new(self.value[key], self.coef[ekey], self.index[ekey])
147
148	def __setitem__(self,key,other):
149		ekey = misc.key_expand(key)
150		if self.is_ad(other):
151			self.value[key] = other.value
152			pad_size = max(self.coef.shape[-1],other.coef.shape[-1])
153			if pad_size>self.coef.shape[-1]:
154				self.coef = _pad_last(self.coef,pad_size)
155				self.index = _pad_last(self.index,pad_size)
156			self.coef[ekey] = _pad_last(other.coef,pad_size)
157			self.index[ekey] = _pad_last(other.index,pad_size)
158		else:
159			self.value[key] = other
160			self.coef[ekey] = 0.
161
162	def reshape(self,shape,order='C'):
163		value = self.value.reshape(shape,order=order)
164		shape2 = value.shape+(self.size_ad,)
165		return self.new(value,self.coef.reshape(shape2,order=order),
166			self.index.reshape(shape2,order=order))
167
168	def broadcast_to(self,shape):
169		shape2 = shape+(self.size_ad,)
170		return self.new(np.broadcast_to(self.value,shape), 
171			np.broadcast_to(self.coef,shape2), np.broadcast_to(self.index,shape2))
172
173	def pad(self,pad_width,*args,constant_values=0,**kwargs):
174		return self.new(
175			np.pad(self.value,pad_width,*args,constant_values=constant_values,**kwargs),
176			np.pad(self.coef, pad_width+((0,0),),*args,constant_values=0,**kwargs),
177			np.pad(self.index,pad_width+((0,0),),*args,constant_values=0,**kwargs))
178	
179	def transpose(self,axes=None):
180		if axes is None: axes = tuple(reversed(range(self.ndim)))
181		axes2 = tuple(axes) +(self.ndim,)
182		return self.new(self.value.transpose(axes),
183			self.coef.transpose(axes2),self.index.transpose(axes2))
184
185	def allclose(self,other,*args,**kwargs):
186		raise ValueError("Unsupported, sorry, please try allclose(a.to_dense(),b.to_dense())")
187		 
188	# Reductions
189	def sum(self,axis=None,out=None,**kwargs):
190		if axis is None: return self.flatten().sum(axis=0,out=out,**kwargs)
191		value = self.value.sum(axis,**kwargs)
192		shape = value.shape +(self.size_ad * self.shape[axis],)
193		coef = np.moveaxis(self.coef, axis,-1).reshape(shape)
194		index = np.moveaxis(self.index, axis,-1).reshape(shape)
195		out = self.new(value,coef,index)
196		return out
197
198	# Conversion
199	def bound_ad(self):
200		return 1+int(cps.max(self.index,initial=-1))
201
202	def to_dense(self,dense_size_ad=None):
203		if self.ndim!=1: 
204			return self.reshape(-1).to_dense(dense_size_ad=dense_size_ad).reshape(self.shape)
205		if dense_size_ad is None: dense_size_ad = self.bound_ad()
206		index = self.index+dense_size_ad*np.arange(self.size)[:,None]
207		coef = np.bincount(index.reshape(-1),self.coef.reshape(-1),dense_size_ad*self.size)
208		return Dense.denseAD(self.value,coef.reshape(self.size,dense_size_ad))
209
210	#Linear algebra
211	def triplets(self,tol=0):
212		"""
213		Returns the triplets defining the sparse linear operator. 
214		- tol : remove coefficients whose magnitude is below this threshold
215		"""
216		xp = cupy_generic.get_array_module(self.value)
217		coef = self.coef.reshape(-1)
218		row = np.broadcast_to(_add_dim(xp.arange(self.size).reshape(self.shape)), self.index.shape).reshape(-1)
219		column = self.index.reshape(-1)
220
221		pos = np.logical_or(np.abs(coef)>tol,np.isnan(coef))
222		return (coef[pos],(row[pos],column[pos]))
223
224	def tangent_operator(self,bound_ad=None,**kwargs):
225		"""
226		The tangent linear operator as a sparse matrix.
227		- **kwargs : passed to triplets
228		"""
229		if bound_ad is None: bound_ad = self.bound_ad()
230		return misc.tocsr(self.triplets(**kwargs),shape=(self.size,bound_ad))
231		
232	def adjoint_operator(self,bound_ad=None,**kwargs):
233		"""
234		The adjoint of the tangent linear operator as a sparse matrix.
235		- **kwargs : passed to triplets
236		"""
237		if bound_ad is None: bound_ad = self.bound_ad()
238		coef,(row,column) = self.triplets(**kwargs)
239		return misc.tocsr((coef,(column,row)),shape=(bound_ad,self.size))
240
241	def solve(self,raw=False):
242		"""
243		Assume that the spAD instance represents the variable y = x + A*delta,
244		where delta is a symbolic perturbation. 
245		Solves the system x + A*delta = 0, assuming compatible shapes.
246		"""
247		mat = self.triplets()
248		rhs = -self.value.flatten()
249		return (mat,rhs) if raw else misc.spsolve(mat,rhs).reshape(self.shape)
250
251	def is_elliptic(self,tol=None,identity_var=None):
252		"""
253		Tests wether the variable encodes a (linear) degenerate elliptic scheme.
254		Output :
255		- sum of the coefficients at each position (must be non-negative for 
256		degenerate ellipticity, positive for strict ellipticity)
257		- maximum of off-diagonal coefficients at each position (must be non-positive)
258		Output (if tol is specified) : 
259		- min_sum >=-tol and max_off <= tol
260		Side effect warning : AD simplification, which is also possibly costly
261		"""
262		self.simplify_ad()
263		min_sum = self.coef.sum(axis=-1)
264
265		rg = (np.arange(self.size).reshape(self.shape+(1,))
266			if identity_var is None else identity_var.index)
267		coef = self.coef.copy()
268		coef[self.index==rg] = -np.inf
269		coef[coef==0.] = -np.inf
270		max_off = coef.max(axis=-1)
271
272		if tol is None: return min_sum,max_off
273		return min_sum.min()>=-tol and max_off.max()<=tol
274
275	@classmethod
276	def concatenate(cls,elems,axis=0):
277		axis1 = axis if axis>=0 else axis-1
278		elems2 = tuple(cls(e) for e in elems)
279		size_ad = max(e.size_ad for e in elems2)
280		return cls( 
281		np.concatenate(tuple(e.value for e in elems2), axis=axis), 
282		np.concatenate(tuple(_pad_last(e.coef,size_ad)  for e in elems2),axis=axis1),
283		np.concatenate(tuple(_pad_last(e.index,size_ad) for e in elems2),axis=axis1))
284
285	# Memory optimization
286	def simplify_ad(self,atol=None,rtol=None):
287		"""
288		Compresses the AD information by merging suitable coefficients, and optionally 
289		removing negligible ones.
290		- atol : absolute tolerance to discard a coefficient. (True -> sensible default.)
291		- rtol : relative tolerance to discard a coefficient (compared to largest in row)
292		Operates in place, but also returns itself.
293		"""
294		# TODO : investigate possible optimizations using np.bincount or scipy.ndimage.sum 
295		if atol is True: atol = np.finfo(self.value.dtype).resolution
296		if rtol is True: rtol = np.finfo(self.value.dtype).resolution
297		if atol is None and rtol is not None: atol=0.
298		if rtol is None and atol is not None: rtol=0.
299
300		if self.size_ad==0: # Nothing to simplify
301			return self
302		if len(self.shape)==0: # Add dimension to scalar-like arrays
303			other = self.reshape((1,))
304			other.simplify_ad(atol=atol,rtol=rtol)
305			other = other.reshape(tuple())
306			self.coef,self.index = other.coef,other.index
307			return self
308
309		if self.cupy_based(): 
310			from .AD_CUDA.simplify_ad import simplify_ad
311			return simplify_ad(self,atol=atol,rtol=rtol)
312
313		if not self.index.flags['WRITEABLE']: 
314			self.index = self.index.copy()
315
316		# Sort indices by increasing values
317		bad_index = np.iinfo(self.index.dtype).max
318		bad_pos = self.coef==0
319		self.index[bad_pos] = bad_index
320		ordering = self.index.argsort(axis=-1)
321		self.coef  = np.take_along_axis(self.coef, ordering,axis=-1)
322		self.index = np.take_along_axis(self.index,ordering,axis=-1)
323
324		# Accumulate coefficients associated with identical indices
325		cum_coef = np.zeros_like(self.value)
326		indices  = np.zeros_like(self.index,shape=self.shape)
327		size_ad = self.size_ad
328		self.coef  = np.moveaxis(self.coef, -1,0)
329		self.index = np.moveaxis(self.index,-1,0)
330		prev_index = np.copy(self.index[0])
331
332		for i in range(size_ad):
333			 # Note : self.index, self.coef change during iterations
334			ind,co = self.index[i],self.coef[i]
335			pos_new_index = np.logical_and(prev_index!=ind, ind!=bad_index)
336			pos_old_index = np.logical_not(pos_new_index)
337			prev_index[pos_new_index] = ind[pos_new_index]
338			cum_coef[pos_new_index]  = co[pos_new_index]
339			cum_coef[pos_old_index] += co[pos_old_index]
340			indices[pos_new_index] += 1
341			indices_exp = np.expand_dims(indices,axis=0)
342			cps.put_along_axis(self.index,indices_exp,prev_index,axis=0)
343			cps.put_along_axis(self.coef, indices_exp,cum_coef,axis=0)
344
345		# Eliminate meaningless coefficients, after largest of indices
346		indices[self.index[0]==bad_index]=-1
347		indices_max = int(np.max(indices,axis=None))
348		size_ad_new = indices_max+1
349		self.coef  = self.coef[:size_ad_new]
350		self.index = self.index[:size_ad_new]
351		if size_ad_new==0:
352			self.coef  = np.moveaxis(self.coef,0,-1)
353			self.index = np.moveaxis(self.index,0,-1)
354			return self
355
356		coef_end  = self.coef[ np.maximum(indices_max,0)]
357		index_end = self.index[np.maximum(indices_max,0)]
358		coef_end[ indices<indices_max] = 0.
359		index_end[indices<indices_max] = -1
360		while np.min(indices,axis=None)<indices_max:
361			indices=np.minimum(indices_max,1+indices)
362			indices_exp = np.expand_dims(indices,axis=0)
363			cps.put_along_axis(self.coef, indices_exp,coef_end,axis=0)
364			cps.put_along_axis(self.index,indices_exp,index_end,axis=0)
365
366		self.coef  = np.moveaxis(self.coef,0,-1)
367		self.index = np.moveaxis(self.index,0,-1)
368		self.coef  = self.coef.reshape( self.shape+(size_ad_new,))
369		self.index = self.index.reshape(self.shape+(size_ad_new,))
370
371		self.index[self.index==-1]=0 # Corresponding coefficient is zero anyway.
372
373		# Optionally remove coefficients below tolerance threshold
374		if atol is not None:
375			tol = atol
376			if rtol!=0:
377				max_coef = np.max(np.abs(self.coef),axis=-1)
378				tol = np.expand_dims( tol + max_coef*rtol, axis=-1)
379
380			bad_pos = np.abs(self.coef) <= tol
381			self.index[bad_pos] = bad_index
382			self.coef[ bad_pos] = 0.
383			ordering = self.index.argsort(axis=-1)
384			self.coef  = np.take_along_axis(self.coef, ordering,axis=-1)
385			self.index = np.take_along_axis(self.index,ordering,axis=-1)
386
387			new_size_ad = self.size_ad - np.min(np.sum(bad_pos,axis=-1))
388			self.coef = self.coef[...,:new_size_ad]
389			self.index = self.index[...,:new_size_ad]
390			self.index[self.index==bad_index]=0 
391
392		return self
393
394# -------- End of class spAD -------
395
396# -------- Factory methods -----
397
398def identity(shape=None,constant=None,shift=0):
399	shape,constant = misc._set_shape_constant(shape,constant)
400	shape2 = shape+(1,)
401	xp = cupy_generic.get_array_module(constant)
402	int_t=cupy_generic.samesize_int_t(constant.dtype)
403	return spAD(constant,np.ones_like(constant,shape=shape2),
404		xp.arange(shift,shift+np.prod(shape,dtype=int),dtype=int_t).reshape(shape2))
405
406def register(inputs,iterables=None,shift=0,ident=identity):
407	if iterables is None:
408		iterables = (tuple,)
409	def reg(a):
410		nonlocal shift
411		a,to_ad = misc.ready_ad(a)
412		if to_ad:
413			result = ident(constant=a,shift=shift)
414			shift += result.size
415			return result
416		else:
417			return a
418	return functional.map_iterables(reg,inputs,iterables)
class spAD(agd.AutomaticDifferentiation.Base.baseAD):
 17class spAD(Base.baseAD):
 18	"""
 19	A class for sparse forward automatic differentiation
 20	"""
 21
 22	def __init__(self,value,coef=None,index=None,broadcast_ad=False):
 23		if self.is_ad(value): # Case where one should just reproduce value
 24			assert coef is None and index is None
 25			self.value,self.coef,self.index = value.value,value.coef,value.index
 26			return
 27		if ad_generic.is_ad(value):
 28			raise Base.ADCastError(f"Attempting to cast {type(value)} to incompatible type spAD")
 29			
 30		assert ((coef is None) and (index is None)) or (coef.shape==index.shape)
 31
 32		self.value = ad_generic.asarray(value)
 33		shape =self.shape
 34		shape2=shape+(0,)
 35		self.coef  = (np.zeros_like(value,shape=shape2) if coef is None 
 36			else misc._test_or_broadcast_ad(coef,shape,broadcast_ad) ) 
 37		self.index = (np.zeros_like(value,shape=shape2,
 38			dtype=cupy_generic.samesize_int_t(value.dtype))  
 39			if index is None else misc._test_or_broadcast_ad(index,shape,broadcast_ad) )
 40
 41	@classmethod
 42	def order(cls): return 1
 43	def copy(self,order='C'):
 44		return self.new(self.value.copy(order=order),
 45			self.coef.copy(order=order),self.index.copy(order=order))
 46	def as_tuple(self): return self.value,self.coef,self.index
 47
 48	def __copy__(self): return self.copy(order='K')
 49	def __deepcopy__(self,*args): 
 50		return self.new(self.value.__deepcopy__(*args),
 51			self.coef.__deepcopy__(*args),self.index.__deepcopy__(*args))
 52
 53	# Representation 
 54	def __iter__(self):
 55		for value,coef,index in zip(self.value,self.coef,self.index):
 56			yield self.new(value,coef,index)
 57
 58	def __str__(self):
 59		return "spAD"+str((self.value,self.coef,self.index))
 60	def __repr__(self):
 61		return "spAD"+repr((self.value,self.coef,self.index))	
 62
 63	# Operators
 64	def as_func(self,h=None):
 65		"""Replaces the symbolic perturbation with h, if specified."""
 66		if h is None: 
 67			lin = self.tangent_operator()
 68			return lambda h : (lin*h).reshape(self.shape) + misc.add_ndim(self.value,h.ndim-1)
 69		value,coef = (misc.add_ndim(e,h.ndim-1) for e in (self.value,self.coef))
 70		return value+(coef*h[self.index]).sum(axis=self.ndim)
 71
 72	def __add__(self,other):
 73		if self.is_ad(other):
 74			value = self.value+other.value
 75			return self.new(value, _concatenate(self.coef,other.coef,value.shape), 
 76				_concatenate(self.index,other.index,value.shape))
 77		else:
 78			return self.new(self.value+other, self.coef, self.index, broadcast_ad=True)
 79
 80	def __sub__(self,other):
 81		if self.is_ad(other):
 82			value = self.value-other.value
 83			return self.new(self.value-other.value, _concatenate(self.coef,-other.coef,value.shape), _concatenate(self.index,other.index,value.shape))
 84		else:
 85			return self.new(self.value-other, self.coef, self.index, broadcast_ad=True)
 86
 87	def __mul__(self,other):
 88		if self.is_ad(other):
 89			value = self.value*other.value
 90			coef1,coef2 = _add_dim(other.value)*self.coef,_add_dim(self.value)*other.coef
 91			index1,index2 = np.broadcast_to(self.index,coef1.shape),np.broadcast_to(other.index,coef2.shape)
 92			return self.new(value,_concatenate(coef1,coef2),_concatenate(index1,index2))
 93		elif self.isndarray(other):
 94			value = self.value*other
 95			coef = _add_dim(other)*self.coef
 96			index = np.broadcast_to(self.index,coef.shape)
 97			return self.new(value,coef,index)
 98		else:
 99			return self.new(self.value*other,other*self.coef,self.index)
100
101	def __truediv__(self,other):
102		if self.is_ad(other):
103			return self.new(self.value/other.value,
104				_concatenate(self.coef*_add_dim(1/other.value),other.coef*_add_dim(-self.value/other.value**2)),
105				_concatenate(self.index,other.index))
106		elif self.isndarray(other):
107			return self.new(self.value/other,self.coef*_add_dim(1./other),self.index)
108		else:
109			return self.new(self.value/other,self.coef/other,self.index)
110
111	__rmul__ = __mul__
112	__radd__ = __add__
113	def __rsub__(self,other): 		return -(self-other)
114	def __rtruediv__(self,other): 	
115		value = other/self.value
116		coef = self.coef*_add_dim(-other/self.value**2)
117		index = np.broadcast_to(self.index,coef.shape)
118		return self.new(value,coef,index)
119
120	def __neg__(self):		return self.new(-self.value,-self.coef,self.index)
121
122	# Math functions
123	def _math_helper(self,deriv):
124		a,b=deriv
125		return self.new(a,_add_dim(b)*self.coef,self.index)
126
127	@classmethod
128	def compose(cls,a,t):
129		assert isinstance(a,Dense.denseAD) and all(cls.is_ad(b) for b in t)
130		lens = tuple(len(b) for b in t)
131		assert a.size_ad == sum(lens)
132		t = tuple(np.moveaxis(b,0,-1) for b in t)
133		a_coefs = np.split(a.coef,np.cumsum(lens[:-1]),axis=-1)
134		def FlattenLast2(arr): return arr.reshape(arr.shape[:-2]+(np.prod(arr.shape[-2:],dtype=int),))
135		coef = tuple(_add_dim(c)*b.coef for c,b in zip(a_coefs,t) )
136		coef = np.concatenate( tuple(FlattenLast2(c) for c in coef), axis=-1)
137		index = np.broadcast_to(np.concatenate( tuple(FlattenLast2(b.index) 
138			for b in t), axis=-1),coef.shape)
139		return cls.new(a.value,coef,index)
140
141	#Indexing
142	@property
143	def size_ad(self):  return self.coef.shape[-1]
144
145	def __getitem__(self,key):
146		ekey = misc.key_expand(key)
147		return self.new(self.value[key], self.coef[ekey], self.index[ekey])
148
149	def __setitem__(self,key,other):
150		ekey = misc.key_expand(key)
151		if self.is_ad(other):
152			self.value[key] = other.value
153			pad_size = max(self.coef.shape[-1],other.coef.shape[-1])
154			if pad_size>self.coef.shape[-1]:
155				self.coef = _pad_last(self.coef,pad_size)
156				self.index = _pad_last(self.index,pad_size)
157			self.coef[ekey] = _pad_last(other.coef,pad_size)
158			self.index[ekey] = _pad_last(other.index,pad_size)
159		else:
160			self.value[key] = other
161			self.coef[ekey] = 0.
162
163	def reshape(self,shape,order='C'):
164		value = self.value.reshape(shape,order=order)
165		shape2 = value.shape+(self.size_ad,)
166		return self.new(value,self.coef.reshape(shape2,order=order),
167			self.index.reshape(shape2,order=order))
168
169	def broadcast_to(self,shape):
170		shape2 = shape+(self.size_ad,)
171		return self.new(np.broadcast_to(self.value,shape), 
172			np.broadcast_to(self.coef,shape2), np.broadcast_to(self.index,shape2))
173
174	def pad(self,pad_width,*args,constant_values=0,**kwargs):
175		return self.new(
176			np.pad(self.value,pad_width,*args,constant_values=constant_values,**kwargs),
177			np.pad(self.coef, pad_width+((0,0),),*args,constant_values=0,**kwargs),
178			np.pad(self.index,pad_width+((0,0),),*args,constant_values=0,**kwargs))
179	
180	def transpose(self,axes=None):
181		if axes is None: axes = tuple(reversed(range(self.ndim)))
182		axes2 = tuple(axes) +(self.ndim,)
183		return self.new(self.value.transpose(axes),
184			self.coef.transpose(axes2),self.index.transpose(axes2))
185
186	def allclose(self,other,*args,**kwargs):
187		raise ValueError("Unsupported, sorry, please try allclose(a.to_dense(),b.to_dense())")
188		 
189	# Reductions
190	def sum(self,axis=None,out=None,**kwargs):
191		if axis is None: return self.flatten().sum(axis=0,out=out,**kwargs)
192		value = self.value.sum(axis,**kwargs)
193		shape = value.shape +(self.size_ad * self.shape[axis],)
194		coef = np.moveaxis(self.coef, axis,-1).reshape(shape)
195		index = np.moveaxis(self.index, axis,-1).reshape(shape)
196		out = self.new(value,coef,index)
197		return out
198
199	# Conversion
200	def bound_ad(self):
201		return 1+int(cps.max(self.index,initial=-1))
202
203	def to_dense(self,dense_size_ad=None):
204		if self.ndim!=1: 
205			return self.reshape(-1).to_dense(dense_size_ad=dense_size_ad).reshape(self.shape)
206		if dense_size_ad is None: dense_size_ad = self.bound_ad()
207		index = self.index+dense_size_ad*np.arange(self.size)[:,None]
208		coef = np.bincount(index.reshape(-1),self.coef.reshape(-1),dense_size_ad*self.size)
209		return Dense.denseAD(self.value,coef.reshape(self.size,dense_size_ad))
210
211	#Linear algebra
212	def triplets(self,tol=0):
213		"""
214		Returns the triplets defining the sparse linear operator. 
215		- tol : remove coefficients whose magnitude is below this threshold
216		"""
217		xp = cupy_generic.get_array_module(self.value)
218		coef = self.coef.reshape(-1)
219		row = np.broadcast_to(_add_dim(xp.arange(self.size).reshape(self.shape)), self.index.shape).reshape(-1)
220		column = self.index.reshape(-1)
221
222		pos = np.logical_or(np.abs(coef)>tol,np.isnan(coef))
223		return (coef[pos],(row[pos],column[pos]))
224
225	def tangent_operator(self,bound_ad=None,**kwargs):
226		"""
227		The tangent linear operator as a sparse matrix.
228		- **kwargs : passed to triplets
229		"""
230		if bound_ad is None: bound_ad = self.bound_ad()
231		return misc.tocsr(self.triplets(**kwargs),shape=(self.size,bound_ad))
232		
233	def adjoint_operator(self,bound_ad=None,**kwargs):
234		"""
235		The adjoint of the tangent linear operator as a sparse matrix.
236		- **kwargs : passed to triplets
237		"""
238		if bound_ad is None: bound_ad = self.bound_ad()
239		coef,(row,column) = self.triplets(**kwargs)
240		return misc.tocsr((coef,(column,row)),shape=(bound_ad,self.size))
241
242	def solve(self,raw=False):
243		"""
244		Assume that the spAD instance represents the variable y = x + A*delta,
245		where delta is a symbolic perturbation. 
246		Solves the system x + A*delta = 0, assuming compatible shapes.
247		"""
248		mat = self.triplets()
249		rhs = -self.value.flatten()
250		return (mat,rhs) if raw else misc.spsolve(mat,rhs).reshape(self.shape)
251
252	def is_elliptic(self,tol=None,identity_var=None):
253		"""
254		Tests wether the variable encodes a (linear) degenerate elliptic scheme.
255		Output :
256		- sum of the coefficients at each position (must be non-negative for 
257		degenerate ellipticity, positive for strict ellipticity)
258		- maximum of off-diagonal coefficients at each position (must be non-positive)
259		Output (if tol is specified) : 
260		- min_sum >=-tol and max_off <= tol
261		Side effect warning : AD simplification, which is also possibly costly
262		"""
263		self.simplify_ad()
264		min_sum = self.coef.sum(axis=-1)
265
266		rg = (np.arange(self.size).reshape(self.shape+(1,))
267			if identity_var is None else identity_var.index)
268		coef = self.coef.copy()
269		coef[self.index==rg] = -np.inf
270		coef[coef==0.] = -np.inf
271		max_off = coef.max(axis=-1)
272
273		if tol is None: return min_sum,max_off
274		return min_sum.min()>=-tol and max_off.max()<=tol
275
276	@classmethod
277	def concatenate(cls,elems,axis=0):
278		axis1 = axis if axis>=0 else axis-1
279		elems2 = tuple(cls(e) for e in elems)
280		size_ad = max(e.size_ad for e in elems2)
281		return cls( 
282		np.concatenate(tuple(e.value for e in elems2), axis=axis), 
283		np.concatenate(tuple(_pad_last(e.coef,size_ad)  for e in elems2),axis=axis1),
284		np.concatenate(tuple(_pad_last(e.index,size_ad) for e in elems2),axis=axis1))
285
286	# Memory optimization
287	def simplify_ad(self,atol=None,rtol=None):
288		"""
289		Compresses the AD information by merging suitable coefficients, and optionally 
290		removing negligible ones.
291		- atol : absolute tolerance to discard a coefficient. (True -> sensible default.)
292		- rtol : relative tolerance to discard a coefficient (compared to largest in row)
293		Operates in place, but also returns itself.
294		"""
295		# TODO : investigate possible optimizations using np.bincount or scipy.ndimage.sum 
296		if atol is True: atol = np.finfo(self.value.dtype).resolution
297		if rtol is True: rtol = np.finfo(self.value.dtype).resolution
298		if atol is None and rtol is not None: atol=0.
299		if rtol is None and atol is not None: rtol=0.
300
301		if self.size_ad==0: # Nothing to simplify
302			return self
303		if len(self.shape)==0: # Add dimension to scalar-like arrays
304			other = self.reshape((1,))
305			other.simplify_ad(atol=atol,rtol=rtol)
306			other = other.reshape(tuple())
307			self.coef,self.index = other.coef,other.index
308			return self
309
310		if self.cupy_based(): 
311			from .AD_CUDA.simplify_ad import simplify_ad
312			return simplify_ad(self,atol=atol,rtol=rtol)
313
314		if not self.index.flags['WRITEABLE']: 
315			self.index = self.index.copy()
316
317		# Sort indices by increasing values
318		bad_index = np.iinfo(self.index.dtype).max
319		bad_pos = self.coef==0
320		self.index[bad_pos] = bad_index
321		ordering = self.index.argsort(axis=-1)
322		self.coef  = np.take_along_axis(self.coef, ordering,axis=-1)
323		self.index = np.take_along_axis(self.index,ordering,axis=-1)
324
325		# Accumulate coefficients associated with identical indices
326		cum_coef = np.zeros_like(self.value)
327		indices  = np.zeros_like(self.index,shape=self.shape)
328		size_ad = self.size_ad
329		self.coef  = np.moveaxis(self.coef, -1,0)
330		self.index = np.moveaxis(self.index,-1,0)
331		prev_index = np.copy(self.index[0])
332
333		for i in range(size_ad):
334			 # Note : self.index, self.coef change during iterations
335			ind,co = self.index[i],self.coef[i]
336			pos_new_index = np.logical_and(prev_index!=ind, ind!=bad_index)
337			pos_old_index = np.logical_not(pos_new_index)
338			prev_index[pos_new_index] = ind[pos_new_index]
339			cum_coef[pos_new_index]  = co[pos_new_index]
340			cum_coef[pos_old_index] += co[pos_old_index]
341			indices[pos_new_index] += 1
342			indices_exp = np.expand_dims(indices,axis=0)
343			cps.put_along_axis(self.index,indices_exp,prev_index,axis=0)
344			cps.put_along_axis(self.coef, indices_exp,cum_coef,axis=0)
345
346		# Eliminate meaningless coefficients, after largest of indices
347		indices[self.index[0]==bad_index]=-1
348		indices_max = int(np.max(indices,axis=None))
349		size_ad_new = indices_max+1
350		self.coef  = self.coef[:size_ad_new]
351		self.index = self.index[:size_ad_new]
352		if size_ad_new==0:
353			self.coef  = np.moveaxis(self.coef,0,-1)
354			self.index = np.moveaxis(self.index,0,-1)
355			return self
356
357		coef_end  = self.coef[ np.maximum(indices_max,0)]
358		index_end = self.index[np.maximum(indices_max,0)]
359		coef_end[ indices<indices_max] = 0.
360		index_end[indices<indices_max] = -1
361		while np.min(indices,axis=None)<indices_max:
362			indices=np.minimum(indices_max,1+indices)
363			indices_exp = np.expand_dims(indices,axis=0)
364			cps.put_along_axis(self.coef, indices_exp,coef_end,axis=0)
365			cps.put_along_axis(self.index,indices_exp,index_end,axis=0)
366
367		self.coef  = np.moveaxis(self.coef,0,-1)
368		self.index = np.moveaxis(self.index,0,-1)
369		self.coef  = self.coef.reshape( self.shape+(size_ad_new,))
370		self.index = self.index.reshape(self.shape+(size_ad_new,))
371
372		self.index[self.index==-1]=0 # Corresponding coefficient is zero anyway.
373
374		# Optionally remove coefficients below tolerance threshold
375		if atol is not None:
376			tol = atol
377			if rtol!=0:
378				max_coef = np.max(np.abs(self.coef),axis=-1)
379				tol = np.expand_dims( tol + max_coef*rtol, axis=-1)
380
381			bad_pos = np.abs(self.coef) <= tol
382			self.index[bad_pos] = bad_index
383			self.coef[ bad_pos] = 0.
384			ordering = self.index.argsort(axis=-1)
385			self.coef  = np.take_along_axis(self.coef, ordering,axis=-1)
386			self.index = np.take_along_axis(self.index,ordering,axis=-1)
387
388			new_size_ad = self.size_ad - np.min(np.sum(bad_pos,axis=-1))
389			self.coef = self.coef[...,:new_size_ad]
390			self.index = self.index[...,:new_size_ad]
391			self.index[self.index==bad_index]=0 
392
393		return self

A class for sparse forward automatic differentiation

spAD(value, coef=None, index=None, broadcast_ad=False)
22	def __init__(self,value,coef=None,index=None,broadcast_ad=False):
23		if self.is_ad(value): # Case where one should just reproduce value
24			assert coef is None and index is None
25			self.value,self.coef,self.index = value.value,value.coef,value.index
26			return
27		if ad_generic.is_ad(value):
28			raise Base.ADCastError(f"Attempting to cast {type(value)} to incompatible type spAD")
29			
30		assert ((coef is None) and (index is None)) or (coef.shape==index.shape)
31
32		self.value = ad_generic.asarray(value)
33		shape =self.shape
34		shape2=shape+(0,)
35		self.coef  = (np.zeros_like(value,shape=shape2) if coef is None 
36			else misc._test_or_broadcast_ad(coef,shape,broadcast_ad) ) 
37		self.index = (np.zeros_like(value,shape=shape2,
38			dtype=cupy_generic.samesize_int_t(value.dtype))  
39			if index is None else misc._test_or_broadcast_ad(index,shape,broadcast_ad) )
value
coef
index
@classmethod
def order(cls):
41	@classmethod
42	def order(cls): return 1
def copy(self, order='C'):
43	def copy(self,order='C'):
44		return self.new(self.value.copy(order=order),
45			self.coef.copy(order=order),self.index.copy(order=order))
def as_tuple(self):
46	def as_tuple(self): return self.value,self.coef,self.index
def as_func(self, h=None):
64	def as_func(self,h=None):
65		"""Replaces the symbolic perturbation with h, if specified."""
66		if h is None: 
67			lin = self.tangent_operator()
68			return lambda h : (lin*h).reshape(self.shape) + misc.add_ndim(self.value,h.ndim-1)
69		value,coef = (misc.add_ndim(e,h.ndim-1) for e in (self.value,self.coef))
70		return value+(coef*h[self.index]).sum(axis=self.ndim)

Replaces the symbolic perturbation with h, if specified.

@classmethod
def compose(cls, a, t):
127	@classmethod
128	def compose(cls,a,t):
129		assert isinstance(a,Dense.denseAD) and all(cls.is_ad(b) for b in t)
130		lens = tuple(len(b) for b in t)
131		assert a.size_ad == sum(lens)
132		t = tuple(np.moveaxis(b,0,-1) for b in t)
133		a_coefs = np.split(a.coef,np.cumsum(lens[:-1]),axis=-1)
134		def FlattenLast2(arr): return arr.reshape(arr.shape[:-2]+(np.prod(arr.shape[-2:],dtype=int),))
135		coef = tuple(_add_dim(c)*b.coef for c,b in zip(a_coefs,t) )
136		coef = np.concatenate( tuple(FlattenLast2(c) for c in coef), axis=-1)
137		index = np.broadcast_to(np.concatenate( tuple(FlattenLast2(b.index) 
138			for b in t), axis=-1),coef.shape)
139		return cls.new(a.value,coef,index)
size_ad
142	@property
143	def size_ad(self):  return self.coef.shape[-1]
def reshape(self, shape, order='C'):
163	def reshape(self,shape,order='C'):
164		value = self.value.reshape(shape,order=order)
165		shape2 = value.shape+(self.size_ad,)
166		return self.new(value,self.coef.reshape(shape2,order=order),
167			self.index.reshape(shape2,order=order))
def broadcast_to(self, shape):
169	def broadcast_to(self,shape):
170		shape2 = shape+(self.size_ad,)
171		return self.new(np.broadcast_to(self.value,shape), 
172			np.broadcast_to(self.coef,shape2), np.broadcast_to(self.index,shape2))
def pad(self, pad_width, *args, constant_values=0, **kwargs):
174	def pad(self,pad_width,*args,constant_values=0,**kwargs):
175		return self.new(
176			np.pad(self.value,pad_width,*args,constant_values=constant_values,**kwargs),
177			np.pad(self.coef, pad_width+((0,0),),*args,constant_values=0,**kwargs),
178			np.pad(self.index,pad_width+((0,0),),*args,constant_values=0,**kwargs))
def transpose(self, axes=None):
180	def transpose(self,axes=None):
181		if axes is None: axes = tuple(reversed(range(self.ndim)))
182		axes2 = tuple(axes) +(self.ndim,)
183		return self.new(self.value.transpose(axes),
184			self.coef.transpose(axes2),self.index.transpose(axes2))
def allclose(self, other, *args, **kwargs):
186	def allclose(self,other,*args,**kwargs):
187		raise ValueError("Unsupported, sorry, please try allclose(a.to_dense(),b.to_dense())")
def sum(self, axis=None, out=None, **kwargs):
190	def sum(self,axis=None,out=None,**kwargs):
191		if axis is None: return self.flatten().sum(axis=0,out=out,**kwargs)
192		value = self.value.sum(axis,**kwargs)
193		shape = value.shape +(self.size_ad * self.shape[axis],)
194		coef = np.moveaxis(self.coef, axis,-1).reshape(shape)
195		index = np.moveaxis(self.index, axis,-1).reshape(shape)
196		out = self.new(value,coef,index)
197		return out
def bound_ad(self):
200	def bound_ad(self):
201		return 1+int(cps.max(self.index,initial=-1))
def to_dense(self, dense_size_ad=None):
203	def to_dense(self,dense_size_ad=None):
204		if self.ndim!=1: 
205			return self.reshape(-1).to_dense(dense_size_ad=dense_size_ad).reshape(self.shape)
206		if dense_size_ad is None: dense_size_ad = self.bound_ad()
207		index = self.index+dense_size_ad*np.arange(self.size)[:,None]
208		coef = np.bincount(index.reshape(-1),self.coef.reshape(-1),dense_size_ad*self.size)
209		return Dense.denseAD(self.value,coef.reshape(self.size,dense_size_ad))
def triplets(self, tol=0):
212	def triplets(self,tol=0):
213		"""
214		Returns the triplets defining the sparse linear operator. 
215		- tol : remove coefficients whose magnitude is below this threshold
216		"""
217		xp = cupy_generic.get_array_module(self.value)
218		coef = self.coef.reshape(-1)
219		row = np.broadcast_to(_add_dim(xp.arange(self.size).reshape(self.shape)), self.index.shape).reshape(-1)
220		column = self.index.reshape(-1)
221
222		pos = np.logical_or(np.abs(coef)>tol,np.isnan(coef))
223		return (coef[pos],(row[pos],column[pos]))

Returns the triplets defining the sparse linear operator.

  • tol : remove coefficients whose magnitude is below this threshold
def tangent_operator(self, bound_ad=None, **kwargs):
225	def tangent_operator(self,bound_ad=None,**kwargs):
226		"""
227		The tangent linear operator as a sparse matrix.
228		- **kwargs : passed to triplets
229		"""
230		if bound_ad is None: bound_ad = self.bound_ad()
231		return misc.tocsr(self.triplets(**kwargs),shape=(self.size,bound_ad))

The tangent linear operator as a sparse matrix.

  • **kwargs : passed to triplets
def adjoint_operator(self, bound_ad=None, **kwargs):
233	def adjoint_operator(self,bound_ad=None,**kwargs):
234		"""
235		The adjoint of the tangent linear operator as a sparse matrix.
236		- **kwargs : passed to triplets
237		"""
238		if bound_ad is None: bound_ad = self.bound_ad()
239		coef,(row,column) = self.triplets(**kwargs)
240		return misc.tocsr((coef,(column,row)),shape=(bound_ad,self.size))

The adjoint of the tangent linear operator as a sparse matrix.

  • **kwargs : passed to triplets
def solve(self, raw=False):
242	def solve(self,raw=False):
243		"""
244		Assume that the spAD instance represents the variable y = x + A*delta,
245		where delta is a symbolic perturbation. 
246		Solves the system x + A*delta = 0, assuming compatible shapes.
247		"""
248		mat = self.triplets()
249		rhs = -self.value.flatten()
250		return (mat,rhs) if raw else misc.spsolve(mat,rhs).reshape(self.shape)

Assume that the spAD instance represents the variable y = x + Adelta, where delta is a symbolic perturbation. Solves the system x + Adelta = 0, assuming compatible shapes.

def is_elliptic(self, tol=None, identity_var=None):
252	def is_elliptic(self,tol=None,identity_var=None):
253		"""
254		Tests wether the variable encodes a (linear) degenerate elliptic scheme.
255		Output :
256		- sum of the coefficients at each position (must be non-negative for 
257		degenerate ellipticity, positive for strict ellipticity)
258		- maximum of off-diagonal coefficients at each position (must be non-positive)
259		Output (if tol is specified) : 
260		- min_sum >=-tol and max_off <= tol
261		Side effect warning : AD simplification, which is also possibly costly
262		"""
263		self.simplify_ad()
264		min_sum = self.coef.sum(axis=-1)
265
266		rg = (np.arange(self.size).reshape(self.shape+(1,))
267			if identity_var is None else identity_var.index)
268		coef = self.coef.copy()
269		coef[self.index==rg] = -np.inf
270		coef[coef==0.] = -np.inf
271		max_off = coef.max(axis=-1)
272
273		if tol is None: return min_sum,max_off
274		return min_sum.min()>=-tol and max_off.max()<=tol

Tests wether the variable encodes a (linear) degenerate elliptic scheme. Output :

  • sum of the coefficients at each position (must be non-negative for degenerate ellipticity, positive for strict ellipticity)
  • maximum of off-diagonal coefficients at each position (must be non-positive) Output (if tol is specified) :
  • min_sum >=-tol and max_off <= tol Side effect warning : AD simplification, which is also possibly costly
@classmethod
def concatenate(cls, elems, axis=0):
276	@classmethod
277	def concatenate(cls,elems,axis=0):
278		axis1 = axis if axis>=0 else axis-1
279		elems2 = tuple(cls(e) for e in elems)
280		size_ad = max(e.size_ad for e in elems2)
281		return cls( 
282		np.concatenate(tuple(e.value for e in elems2), axis=axis), 
283		np.concatenate(tuple(_pad_last(e.coef,size_ad)  for e in elems2),axis=axis1),
284		np.concatenate(tuple(_pad_last(e.index,size_ad) for e in elems2),axis=axis1))
def simplify_ad(self, atol=None, rtol=None):
287	def simplify_ad(self,atol=None,rtol=None):
288		"""
289		Compresses the AD information by merging suitable coefficients, and optionally 
290		removing negligible ones.
291		- atol : absolute tolerance to discard a coefficient. (True -> sensible default.)
292		- rtol : relative tolerance to discard a coefficient (compared to largest in row)
293		Operates in place, but also returns itself.
294		"""
295		# TODO : investigate possible optimizations using np.bincount or scipy.ndimage.sum 
296		if atol is True: atol = np.finfo(self.value.dtype).resolution
297		if rtol is True: rtol = np.finfo(self.value.dtype).resolution
298		if atol is None and rtol is not None: atol=0.
299		if rtol is None and atol is not None: rtol=0.
300
301		if self.size_ad==0: # Nothing to simplify
302			return self
303		if len(self.shape)==0: # Add dimension to scalar-like arrays
304			other = self.reshape((1,))
305			other.simplify_ad(atol=atol,rtol=rtol)
306			other = other.reshape(tuple())
307			self.coef,self.index = other.coef,other.index
308			return self
309
310		if self.cupy_based(): 
311			from .AD_CUDA.simplify_ad import simplify_ad
312			return simplify_ad(self,atol=atol,rtol=rtol)
313
314		if not self.index.flags['WRITEABLE']: 
315			self.index = self.index.copy()
316
317		# Sort indices by increasing values
318		bad_index = np.iinfo(self.index.dtype).max
319		bad_pos = self.coef==0
320		self.index[bad_pos] = bad_index
321		ordering = self.index.argsort(axis=-1)
322		self.coef  = np.take_along_axis(self.coef, ordering,axis=-1)
323		self.index = np.take_along_axis(self.index,ordering,axis=-1)
324
325		# Accumulate coefficients associated with identical indices
326		cum_coef = np.zeros_like(self.value)
327		indices  = np.zeros_like(self.index,shape=self.shape)
328		size_ad = self.size_ad
329		self.coef  = np.moveaxis(self.coef, -1,0)
330		self.index = np.moveaxis(self.index,-1,0)
331		prev_index = np.copy(self.index[0])
332
333		for i in range(size_ad):
334			 # Note : self.index, self.coef change during iterations
335			ind,co = self.index[i],self.coef[i]
336			pos_new_index = np.logical_and(prev_index!=ind, ind!=bad_index)
337			pos_old_index = np.logical_not(pos_new_index)
338			prev_index[pos_new_index] = ind[pos_new_index]
339			cum_coef[pos_new_index]  = co[pos_new_index]
340			cum_coef[pos_old_index] += co[pos_old_index]
341			indices[pos_new_index] += 1
342			indices_exp = np.expand_dims(indices,axis=0)
343			cps.put_along_axis(self.index,indices_exp,prev_index,axis=0)
344			cps.put_along_axis(self.coef, indices_exp,cum_coef,axis=0)
345
346		# Eliminate meaningless coefficients, after largest of indices
347		indices[self.index[0]==bad_index]=-1
348		indices_max = int(np.max(indices,axis=None))
349		size_ad_new = indices_max+1
350		self.coef  = self.coef[:size_ad_new]
351		self.index = self.index[:size_ad_new]
352		if size_ad_new==0:
353			self.coef  = np.moveaxis(self.coef,0,-1)
354			self.index = np.moveaxis(self.index,0,-1)
355			return self
356
357		coef_end  = self.coef[ np.maximum(indices_max,0)]
358		index_end = self.index[np.maximum(indices_max,0)]
359		coef_end[ indices<indices_max] = 0.
360		index_end[indices<indices_max] = -1
361		while np.min(indices,axis=None)<indices_max:
362			indices=np.minimum(indices_max,1+indices)
363			indices_exp = np.expand_dims(indices,axis=0)
364			cps.put_along_axis(self.coef, indices_exp,coef_end,axis=0)
365			cps.put_along_axis(self.index,indices_exp,index_end,axis=0)
366
367		self.coef  = np.moveaxis(self.coef,0,-1)
368		self.index = np.moveaxis(self.index,0,-1)
369		self.coef  = self.coef.reshape( self.shape+(size_ad_new,))
370		self.index = self.index.reshape(self.shape+(size_ad_new,))
371
372		self.index[self.index==-1]=0 # Corresponding coefficient is zero anyway.
373
374		# Optionally remove coefficients below tolerance threshold
375		if atol is not None:
376			tol = atol
377			if rtol!=0:
378				max_coef = np.max(np.abs(self.coef),axis=-1)
379				tol = np.expand_dims( tol + max_coef*rtol, axis=-1)
380
381			bad_pos = np.abs(self.coef) <= tol
382			self.index[bad_pos] = bad_index
383			self.coef[ bad_pos] = 0.
384			ordering = self.index.argsort(axis=-1)
385			self.coef  = np.take_along_axis(self.coef, ordering,axis=-1)
386			self.index = np.take_along_axis(self.index,ordering,axis=-1)
387
388			new_size_ad = self.size_ad - np.min(np.sum(bad_pos,axis=-1))
389			self.coef = self.coef[...,:new_size_ad]
390			self.index = self.index[...,:new_size_ad]
391			self.index[self.index==bad_index]=0 
392
393		return self

Compresses the AD information by merging suitable coefficients, and optionally removing negligible ones.

  • atol : absolute tolerance to discard a coefficient. (True -> sensible default.)
  • rtol : relative tolerance to discard a coefficient (compared to largest in row) Operates in place, but also returns itself.
def identity(shape=None, constant=None, shift=0):
399def identity(shape=None,constant=None,shift=0):
400	shape,constant = misc._set_shape_constant(shape,constant)
401	shape2 = shape+(1,)
402	xp = cupy_generic.get_array_module(constant)
403	int_t=cupy_generic.samesize_int_t(constant.dtype)
404	return spAD(constant,np.ones_like(constant,shape=shape2),
405		xp.arange(shift,shift+np.prod(shape,dtype=int),dtype=int_t).reshape(shape2))
def register(inputs, iterables=None, shift=0, ident=<function identity>):
407def register(inputs,iterables=None,shift=0,ident=identity):
408	if iterables is None:
409		iterables = (tuple,)
410	def reg(a):
411		nonlocal shift
412		a,to_ad = misc.ready_ad(a)
413		if to_ad:
414			result = ident(constant=a,shift=shift)
415			shift += result.size
416			return result
417		else:
418			return a
419	return functional.map_iterables(reg,inputs,iterables)