agd.AutomaticDifferentiation.Dense
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 functools 5import numpy as np 6from . import functional 7from . import Base 8from . import ad_generic 9from . import misc 10 11 12_add_dim = misc._add_dim; _add_coef=misc._add_coef 13 14class denseAD(Base.baseAD): 15 """ 16 A class for dense forward automatic differentiation 17 """ 18 19 def __init__(self,value,coef=None,broadcast_ad=False): 20 if self.is_ad(value): # Case where one should just reproduce value 21 assert coef is None 22 self.value,self.coef=value.value,value.coef 23 return 24 if ad_generic.is_ad(value): 25 raise Base.ADCastError(f"Attempting to cast {type(value)} to incompatible type denseAD") 26 self.value = ad_generic.asarray(value) 27 self.coef = (np.zeros_like(value,shape=self.shape+(0,)) if coef is None 28 else misc._test_or_broadcast_ad(coef,self.shape,broadcast_ad) ) 29 30 @classmethod 31 def order(cls): return 1 32 def copy(self,order='C'): 33 return self.new(self.value.copy(order=order),self.coef.copy(order=order)) 34 def as_tuple(self): return self.value,self.coef 35 36 # Representation 37 def __iter__(self): 38 for value,coef in zip(self.value,self.coef): 39 yield self.new(value,coef) 40 def __str__(self): 41 return "denseAD("+str(self.value)+","+misc._prep_nl(str(self.coef))+")" 42 def __repr__(self): 43 return "denseAD("+repr(self.value)+","+misc._prep_nl(repr(self.coef))+")" 44 45 # Operators 46 def as_func(self,h): 47 """Replaces the symbolic perturbation with h""" 48 value,coef = (misc.add_ndim(e,h.ndim-1) for e in (self.value,self.coef)) 49 return value+(coef*h).sum(axis=self.ndim) 50 51 def __add__(self,other): 52 if self.is_ad(other): 53 return self.new(self.value+other.value, _add_coef(self.coef,other.coef)) 54 else: 55 return self.new(self.value+other, self.coef, broadcast_ad=True) 56 57 def __sub__(self,other): 58 if self.is_ad(other): 59 return self.new(self.value-other.value, _add_coef(self.coef,-other.coef)) 60 else: 61 return self.new(self.value-other, self.coef, broadcast_ad=True) 62 63 def __mul__(self,other): 64 if self.is_ad(other): 65 return self.new(self.value*other.value,_add_coef(_add_dim(other.value)*self.coef, 66 _add_dim(self.value)*other.coef)) 67 elif self.isndarray(other): 68 return self.new(self.value*other,_add_dim(other)*self.coef) 69 else: 70 return self.new(self.value*other,other*self.coef) 71 72 def __truediv__(self,other): 73 if self.is_ad(other): 74 return self.new(self.value/other.value, 75 _add_coef(_add_dim(1/other.value)*self.coef, 76 _add_dim(-self.value/other.value**2)*other.coef)) 77 elif self.isndarray(other): 78 return self.new(self.value/other,_add_dim(1./other)*self.coef) 79 else: 80 return self.new(self.value/other,(1./other)*self.coef) 81 82 __rmul__ = __mul__ 83 __radd__ = __add__ 84 def __rsub__(self,other): return -(self-other) 85 def __rtruediv__(self,other): return self.new(other/self.value, 86 _add_dim(-other/self.value**2)*self.coef) 87 88 def __neg__(self): return self.new(-self.value,-self.coef) 89 90 # Math functions 91 def _math_helper(self,deriv): 92 a,b=deriv 93 return self.new(a,_add_dim(b)*self.coef) 94 95 @classmethod 96 def compose(cls,a,t): 97 assert cls.is_ad(a) and all(cls.is_ad(b) for b in t) 98 b = np.moveaxis(cls.concatenate(t,axis=0),0,-1) 99 coef = (_add_dim(a.coef)*b.coef).sum(axis=-2) 100 return cls(a.value,coef) 101 102 #Indexing 103 @property 104 def size_ad(self): return self.coef.shape[-1] 105 106 def gradient(self,i=None): 107 """Returns the gradient, or the i-th component of the gradient if specified.""" 108 grad = np.moveaxis(self.coef,-1,0) 109 return grad if i is None else grad[i] 110 111 def __getitem__(self,key): 112 ekey = misc.key_expand(key) 113 return self.new(self.value[key], self.coef[ekey]) 114 115 def __setitem__(self,key,other): 116 ekey = misc.key_expand(key) 117 if self.is_ad(other): 118 if other.size_ad==0: return self.__setitem__(key,other.value) 119 elif self.size_ad==0: 120 self.coef=np.zeros_like(self.value,shape=self.shape+(other.size_ad,)) 121 self.value[key] = other.value 122 self.coef[ekey] = other.coef 123 else: 124 self.value[key] = other 125 self.coef[ekey] = 0. 126 127 def reshape(self,shape,order='C'): 128 value = self.value.reshape(shape,order=order) 129 return self.new(value,self.coef.reshape(value.shape+(self.size_ad,),order=order)) 130 131 def broadcast_to(self,shape): 132 shape2 = shape+(self.size_ad,) 133 return self.new(np.broadcast_to(self.value,shape), np.broadcast_to(self.coef,shape2) ) 134 135 def pad(self,pad_width,*args,constant_values=0,**kwargs): 136 return self.new( 137 np.pad(self.value,pad_width,*args,constant_values=constant_values,**kwargs), 138 np.pad(self.coef,pad_width+((0,0),),*args,constant_values=0,**kwargs)) 139 140 def transpose(self,axes=None): 141 if axes is None: axes = tuple(reversed(range(self.ndim))) 142 axes2 = tuple(axes)+(self.ndim,) 143 return self.new(self.value.transpose(axes),self.coef.transpose(axes2)) 144 145 def allclose(self,other,*args,**kwargs): 146 return np.allclose(self.value,other.value,*args,**kwargs) and \ 147 np.allclose(self.coef, other.coef, *args,**kwargs) 148 149 # Reductions 150 def sum(self,axis=None,out=None,**kwargs): 151 if axis is None: return self.flatten().sum(axis=0,out=out,**kwargs) 152 axis=misc.normalize_axis(axis,self.ndim) 153 out = self.new(self.value.sum(axis,**kwargs), self.coef.sum(axis,**kwargs)) 154 return out 155 156 # Numerical 157 def solve(self,shape_free=None,shape_bound=None): 158 shape_free,shape_bound = ad_generic._set_shape_free_bound(self.shape,shape_free,shape_bound) 159 assert np.prod(shape_free)==self.size_ad 160 v = np.moveaxis(np.reshape(self.value,(self.size_ad,)+shape_bound),0,-1) 161 a = np.moveaxis(np.reshape(self.coef,(self.size_ad,)+shape_bound+(self.size_ad,)),0,-2) 162 return -np.reshape(np.moveaxis(np.linalg.solve(a,v),-1,0),self.shape) 163 164 @classmethod 165 def concatenate(cls,elems,axis=0): 166 axis1 = axis if axis>=0 else axis-1 167 elems2 = tuple(cls(e) for e in elems) 168 size_ad = max(e.size_ad for e in elems2) 169 assert all((e.size_ad==size_ad or e.size_ad==0) for e in elems2) 170 return cls( 171 np.concatenate(tuple(e.value for e in elems2), axis=axis), 172 np.concatenate(tuple(e.coef if e.size_ad==size_ad else 173 np.zeros_like(e.value,shape=e.shape+(size_ad,)) for e in elems2),axis=axis1)) 174 175 def associate(self,squeeze_free_dims=-1,squeeze_bound_dims=-1): 176 from . import associate 177 sq_free = squeeze_free_dims 178 sq_free1= sq_free if sq_free>=0 else (sq_free-1) 179 value = associate(self.value,sq_free, squeeze_bound_dims) 180 coef = associate(self.coef, sq_free1,squeeze_bound_dims) 181 coef = np.moveaxis(coef,self.ndim if sq_free is None else (self.ndim-1),-1) 182 return self.new(value,coef) 183 184 def apply_linear_operator(self,op): 185 if isinstance(op,denseAD_Lin): return op(self) 186 val = op(self.value) 187 # The next case is also for denseAD_Lin, but when it is hidden within a function. 188 # Note : incurs a recomputation of op(self.value)... 189 if self.is_ad(val): return op(self) 190 return self.new(val,misc.apply_linear_operator(op,self.coef,flatten_ndim=1)) 191 192# -------- End of class denseAD ------- 193 194# -------- Factory methods ----- 195 196 197def identity(shape=None,shape_free=None,shape_bound=None,constant=None,shift=(0,0)): 198 """ 199 Creates a dense AD variable with independent symbolic perturbations for each coordinate 200 (unless some are tied together as specified by shape_free and shape_bound) 201 """ 202 shape,constant = misc._set_shape_constant(shape,constant) 203 shape_free,shape_bound = ad_generic._set_shape_free_bound(shape,shape_free,shape_bound) 204 205 ndim_elem = len(shape)-len(shape_bound) 206 shape_elem = shape[:ndim_elem] 207 size_elem = int(np.prod(shape_elem)) 208 size_ad = shift[0]+size_elem+shift[1] 209 coef1 = np.zeros_like(constant,shape=(size_elem,size_ad)) 210 for i in range(size_elem): 211 coef1[i,shift[0]+i]=1. 212 coef1 = coef1.reshape(shape_elem+(1,)*len(shape_bound)+(size_ad,)) 213 if coef1.shape[:-1]!=constant.shape: 214 coef1 = np.broadcast_to(coef1,shape+(size_ad,)) 215 return denseAD(constant,coef1) 216 217def register(inputs,iterables=None,shape_bound=None,shift=(0,0),ident=identity,considered=None): 218 """ 219 Creates a series of dense AD variables with independent symbolic perturbations for each coordinate, 220 and adequate intermediate shifts. 221 """ 222 if iterables is None: 223 iterables = (tuple,) 224 boundsize = 1 if shape_bound is None else np.prod(shape_bound,dtype=int) 225 def is_considered(a): 226 return considered is None or a in considered 227 228 start=shift[0] 229 starts = [] 230 def setstart(a): 231 nonlocal start,starts 232 if considered is None or any(a is val for val in considered): 233 a,to_ad = misc.ready_ad(a) 234 if to_ad: 235 starts.append(start) 236 start += a.size//boundsize 237 return a 238 starts.append(None) 239 return a 240 inputs = functional.map_iterables(setstart,inputs,iterables) 241 242 end = start+shift[1] 243 244 starts_it = iter(starts) 245 def setad(a): 246 start = next(starts_it) 247 if start is None: return a 248 else: return ident(constant=a,shift=(start,end-start-a.size//boundsize), 249 shape_bound=shape_bound) 250 return functional.map_iterables(setad,inputs,iterables) 251 252 253# ------- class denseAD_Lin -------- 254class denseAD_Lin: 255 """ 256 A class implementing a linear operator L with an AD part δL, and rule 257 (L+δL)(u+δu) = L(u) + (δL(u) + L(δu)) + o(δ^2). 258 """ 259 260 def __init__(self,value,coef): 261 """ 262 - value : Some linear operator L 263 - coef : A list of linear operators δL 264 """ 265 self.value = value 266 self.coef = coef 267 268 def __str__(self): return "denseAD_Lin" + str((self.value,self.coef)) 269 def __repr__(self): return "denseAD_Lin" + repr((self.value,self.coef)) 270 271 @property 272 def size_ad(): return len(self.coef) 273 @property 274 def _value(self): return as_callable(self.value) 275 @property 276 def _coef(self): return [as_callable(δL) for δL in self.coef] 277 278 def __call__(self,other): 279 if isinstance(other,denseAD): 280 if other.size_ad==0: return self.__call__(other.value) 281 res = other.apply_linear_operator(self._value) 282 res.coef += np.stack([δL(other.value) for δL in self._coef],axis=-1) 283 return res 284 else: 285 return denseAD(self._value(other), 286 np.stack([δL(other) for δL in self._coef],axis=-1)) 287 288 def __mul__(self,other): return self.__call__(other) 289 290def as_callable(L): 291 """Make matrices and sparse matrices callable""" 292 if callable(L): return L 293 return lambda x:np.dot(L,x) if isinstance(L,np.ndarray) else (L*x)
15class denseAD(Base.baseAD): 16 """ 17 A class for dense forward automatic differentiation 18 """ 19 20 def __init__(self,value,coef=None,broadcast_ad=False): 21 if self.is_ad(value): # Case where one should just reproduce value 22 assert coef is None 23 self.value,self.coef=value.value,value.coef 24 return 25 if ad_generic.is_ad(value): 26 raise Base.ADCastError(f"Attempting to cast {type(value)} to incompatible type denseAD") 27 self.value = ad_generic.asarray(value) 28 self.coef = (np.zeros_like(value,shape=self.shape+(0,)) if coef is None 29 else misc._test_or_broadcast_ad(coef,self.shape,broadcast_ad) ) 30 31 @classmethod 32 def order(cls): return 1 33 def copy(self,order='C'): 34 return self.new(self.value.copy(order=order),self.coef.copy(order=order)) 35 def as_tuple(self): return self.value,self.coef 36 37 # Representation 38 def __iter__(self): 39 for value,coef in zip(self.value,self.coef): 40 yield self.new(value,coef) 41 def __str__(self): 42 return "denseAD("+str(self.value)+","+misc._prep_nl(str(self.coef))+")" 43 def __repr__(self): 44 return "denseAD("+repr(self.value)+","+misc._prep_nl(repr(self.coef))+")" 45 46 # Operators 47 def as_func(self,h): 48 """Replaces the symbolic perturbation with h""" 49 value,coef = (misc.add_ndim(e,h.ndim-1) for e in (self.value,self.coef)) 50 return value+(coef*h).sum(axis=self.ndim) 51 52 def __add__(self,other): 53 if self.is_ad(other): 54 return self.new(self.value+other.value, _add_coef(self.coef,other.coef)) 55 else: 56 return self.new(self.value+other, self.coef, broadcast_ad=True) 57 58 def __sub__(self,other): 59 if self.is_ad(other): 60 return self.new(self.value-other.value, _add_coef(self.coef,-other.coef)) 61 else: 62 return self.new(self.value-other, self.coef, broadcast_ad=True) 63 64 def __mul__(self,other): 65 if self.is_ad(other): 66 return self.new(self.value*other.value,_add_coef(_add_dim(other.value)*self.coef, 67 _add_dim(self.value)*other.coef)) 68 elif self.isndarray(other): 69 return self.new(self.value*other,_add_dim(other)*self.coef) 70 else: 71 return self.new(self.value*other,other*self.coef) 72 73 def __truediv__(self,other): 74 if self.is_ad(other): 75 return self.new(self.value/other.value, 76 _add_coef(_add_dim(1/other.value)*self.coef, 77 _add_dim(-self.value/other.value**2)*other.coef)) 78 elif self.isndarray(other): 79 return self.new(self.value/other,_add_dim(1./other)*self.coef) 80 else: 81 return self.new(self.value/other,(1./other)*self.coef) 82 83 __rmul__ = __mul__ 84 __radd__ = __add__ 85 def __rsub__(self,other): return -(self-other) 86 def __rtruediv__(self,other): return self.new(other/self.value, 87 _add_dim(-other/self.value**2)*self.coef) 88 89 def __neg__(self): return self.new(-self.value,-self.coef) 90 91 # Math functions 92 def _math_helper(self,deriv): 93 a,b=deriv 94 return self.new(a,_add_dim(b)*self.coef) 95 96 @classmethod 97 def compose(cls,a,t): 98 assert cls.is_ad(a) and all(cls.is_ad(b) for b in t) 99 b = np.moveaxis(cls.concatenate(t,axis=0),0,-1) 100 coef = (_add_dim(a.coef)*b.coef).sum(axis=-2) 101 return cls(a.value,coef) 102 103 #Indexing 104 @property 105 def size_ad(self): return self.coef.shape[-1] 106 107 def gradient(self,i=None): 108 """Returns the gradient, or the i-th component of the gradient if specified.""" 109 grad = np.moveaxis(self.coef,-1,0) 110 return grad if i is None else grad[i] 111 112 def __getitem__(self,key): 113 ekey = misc.key_expand(key) 114 return self.new(self.value[key], self.coef[ekey]) 115 116 def __setitem__(self,key,other): 117 ekey = misc.key_expand(key) 118 if self.is_ad(other): 119 if other.size_ad==0: return self.__setitem__(key,other.value) 120 elif self.size_ad==0: 121 self.coef=np.zeros_like(self.value,shape=self.shape+(other.size_ad,)) 122 self.value[key] = other.value 123 self.coef[ekey] = other.coef 124 else: 125 self.value[key] = other 126 self.coef[ekey] = 0. 127 128 def reshape(self,shape,order='C'): 129 value = self.value.reshape(shape,order=order) 130 return self.new(value,self.coef.reshape(value.shape+(self.size_ad,),order=order)) 131 132 def broadcast_to(self,shape): 133 shape2 = shape+(self.size_ad,) 134 return self.new(np.broadcast_to(self.value,shape), np.broadcast_to(self.coef,shape2) ) 135 136 def pad(self,pad_width,*args,constant_values=0,**kwargs): 137 return self.new( 138 np.pad(self.value,pad_width,*args,constant_values=constant_values,**kwargs), 139 np.pad(self.coef,pad_width+((0,0),),*args,constant_values=0,**kwargs)) 140 141 def transpose(self,axes=None): 142 if axes is None: axes = tuple(reversed(range(self.ndim))) 143 axes2 = tuple(axes)+(self.ndim,) 144 return self.new(self.value.transpose(axes),self.coef.transpose(axes2)) 145 146 def allclose(self,other,*args,**kwargs): 147 return np.allclose(self.value,other.value,*args,**kwargs) and \ 148 np.allclose(self.coef, other.coef, *args,**kwargs) 149 150 # Reductions 151 def sum(self,axis=None,out=None,**kwargs): 152 if axis is None: return self.flatten().sum(axis=0,out=out,**kwargs) 153 axis=misc.normalize_axis(axis,self.ndim) 154 out = self.new(self.value.sum(axis,**kwargs), self.coef.sum(axis,**kwargs)) 155 return out 156 157 # Numerical 158 def solve(self,shape_free=None,shape_bound=None): 159 shape_free,shape_bound = ad_generic._set_shape_free_bound(self.shape,shape_free,shape_bound) 160 assert np.prod(shape_free)==self.size_ad 161 v = np.moveaxis(np.reshape(self.value,(self.size_ad,)+shape_bound),0,-1) 162 a = np.moveaxis(np.reshape(self.coef,(self.size_ad,)+shape_bound+(self.size_ad,)),0,-2) 163 return -np.reshape(np.moveaxis(np.linalg.solve(a,v),-1,0),self.shape) 164 165 @classmethod 166 def concatenate(cls,elems,axis=0): 167 axis1 = axis if axis>=0 else axis-1 168 elems2 = tuple(cls(e) for e in elems) 169 size_ad = max(e.size_ad for e in elems2) 170 assert all((e.size_ad==size_ad or e.size_ad==0) for e in elems2) 171 return cls( 172 np.concatenate(tuple(e.value for e in elems2), axis=axis), 173 np.concatenate(tuple(e.coef if e.size_ad==size_ad else 174 np.zeros_like(e.value,shape=e.shape+(size_ad,)) for e in elems2),axis=axis1)) 175 176 def associate(self,squeeze_free_dims=-1,squeeze_bound_dims=-1): 177 from . import associate 178 sq_free = squeeze_free_dims 179 sq_free1= sq_free if sq_free>=0 else (sq_free-1) 180 value = associate(self.value,sq_free, squeeze_bound_dims) 181 coef = associate(self.coef, sq_free1,squeeze_bound_dims) 182 coef = np.moveaxis(coef,self.ndim if sq_free is None else (self.ndim-1),-1) 183 return self.new(value,coef) 184 185 def apply_linear_operator(self,op): 186 if isinstance(op,denseAD_Lin): return op(self) 187 val = op(self.value) 188 # The next case is also for denseAD_Lin, but when it is hidden within a function. 189 # Note : incurs a recomputation of op(self.value)... 190 if self.is_ad(val): return op(self) 191 return self.new(val,misc.apply_linear_operator(op,self.coef,flatten_ndim=1))
A class for dense forward automatic differentiation
denseAD(value, coef=None, broadcast_ad=False)
20 def __init__(self,value,coef=None,broadcast_ad=False): 21 if self.is_ad(value): # Case where one should just reproduce value 22 assert coef is None 23 self.value,self.coef=value.value,value.coef 24 return 25 if ad_generic.is_ad(value): 26 raise Base.ADCastError(f"Attempting to cast {type(value)} to incompatible type denseAD") 27 self.value = ad_generic.asarray(value) 28 self.coef = (np.zeros_like(value,shape=self.shape+(0,)) if coef is None 29 else misc._test_or_broadcast_ad(coef,self.shape,broadcast_ad) )
def
as_func(self, h):
47 def as_func(self,h): 48 """Replaces the symbolic perturbation with h""" 49 value,coef = (misc.add_ndim(e,h.ndim-1) for e in (self.value,self.coef)) 50 return value+(coef*h).sum(axis=self.ndim)
Replaces the symbolic perturbation with h
def
gradient(self, i=None):
107 def gradient(self,i=None): 108 """Returns the gradient, or the i-th component of the gradient if specified.""" 109 grad = np.moveaxis(self.coef,-1,0) 110 return grad if i is None else grad[i]
Returns the gradient, or the i-th component of the gradient if specified.
def
solve(self, shape_free=None, shape_bound=None):
158 def solve(self,shape_free=None,shape_bound=None): 159 shape_free,shape_bound = ad_generic._set_shape_free_bound(self.shape,shape_free,shape_bound) 160 assert np.prod(shape_free)==self.size_ad 161 v = np.moveaxis(np.reshape(self.value,(self.size_ad,)+shape_bound),0,-1) 162 a = np.moveaxis(np.reshape(self.coef,(self.size_ad,)+shape_bound+(self.size_ad,)),0,-2) 163 return -np.reshape(np.moveaxis(np.linalg.solve(a,v),-1,0),self.shape)
@classmethod
def
concatenate(cls, elems, axis=0):
165 @classmethod 166 def concatenate(cls,elems,axis=0): 167 axis1 = axis if axis>=0 else axis-1 168 elems2 = tuple(cls(e) for e in elems) 169 size_ad = max(e.size_ad for e in elems2) 170 assert all((e.size_ad==size_ad or e.size_ad==0) for e in elems2) 171 return cls( 172 np.concatenate(tuple(e.value for e in elems2), axis=axis), 173 np.concatenate(tuple(e.coef if e.size_ad==size_ad else 174 np.zeros_like(e.value,shape=e.shape+(size_ad,)) for e in elems2),axis=axis1))
def
associate(self, squeeze_free_dims=-1, squeeze_bound_dims=-1):
176 def associate(self,squeeze_free_dims=-1,squeeze_bound_dims=-1): 177 from . import associate 178 sq_free = squeeze_free_dims 179 sq_free1= sq_free if sq_free>=0 else (sq_free-1) 180 value = associate(self.value,sq_free, squeeze_bound_dims) 181 coef = associate(self.coef, sq_free1,squeeze_bound_dims) 182 coef = np.moveaxis(coef,self.ndim if sq_free is None else (self.ndim-1),-1) 183 return self.new(value,coef)
def
apply_linear_operator(self, op):
185 def apply_linear_operator(self,op): 186 if isinstance(op,denseAD_Lin): return op(self) 187 val = op(self.value) 188 # The next case is also for denseAD_Lin, but when it is hidden within a function. 189 # Note : incurs a recomputation of op(self.value)... 190 if self.is_ad(val): return op(self) 191 return self.new(val,misc.apply_linear_operator(op,self.coef,flatten_ndim=1))
def
identity( shape=None, shape_free=None, shape_bound=None, constant=None, shift=(0, 0)):
198def identity(shape=None,shape_free=None,shape_bound=None,constant=None,shift=(0,0)): 199 """ 200 Creates a dense AD variable with independent symbolic perturbations for each coordinate 201 (unless some are tied together as specified by shape_free and shape_bound) 202 """ 203 shape,constant = misc._set_shape_constant(shape,constant) 204 shape_free,shape_bound = ad_generic._set_shape_free_bound(shape,shape_free,shape_bound) 205 206 ndim_elem = len(shape)-len(shape_bound) 207 shape_elem = shape[:ndim_elem] 208 size_elem = int(np.prod(shape_elem)) 209 size_ad = shift[0]+size_elem+shift[1] 210 coef1 = np.zeros_like(constant,shape=(size_elem,size_ad)) 211 for i in range(size_elem): 212 coef1[i,shift[0]+i]=1. 213 coef1 = coef1.reshape(shape_elem+(1,)*len(shape_bound)+(size_ad,)) 214 if coef1.shape[:-1]!=constant.shape: 215 coef1 = np.broadcast_to(coef1,shape+(size_ad,)) 216 return denseAD(constant,coef1)
Creates a dense AD variable with independent symbolic perturbations for each coordinate (unless some are tied together as specified by shape_free and shape_bound)
def
register( inputs, iterables=None, shape_bound=None, shift=(0, 0), ident=<function identity>, considered=None):
218def register(inputs,iterables=None,shape_bound=None,shift=(0,0),ident=identity,considered=None): 219 """ 220 Creates a series of dense AD variables with independent symbolic perturbations for each coordinate, 221 and adequate intermediate shifts. 222 """ 223 if iterables is None: 224 iterables = (tuple,) 225 boundsize = 1 if shape_bound is None else np.prod(shape_bound,dtype=int) 226 def is_considered(a): 227 return considered is None or a in considered 228 229 start=shift[0] 230 starts = [] 231 def setstart(a): 232 nonlocal start,starts 233 if considered is None or any(a is val for val in considered): 234 a,to_ad = misc.ready_ad(a) 235 if to_ad: 236 starts.append(start) 237 start += a.size//boundsize 238 return a 239 starts.append(None) 240 return a 241 inputs = functional.map_iterables(setstart,inputs,iterables) 242 243 end = start+shift[1] 244 245 starts_it = iter(starts) 246 def setad(a): 247 start = next(starts_it) 248 if start is None: return a 249 else: return ident(constant=a,shift=(start,end-start-a.size//boundsize), 250 shape_bound=shape_bound) 251 return functional.map_iterables(setad,inputs,iterables)
Creates a series of dense AD variables with independent symbolic perturbations for each coordinate, and adequate intermediate shifts.
class
denseAD_Lin:
255class denseAD_Lin: 256 """ 257 A class implementing a linear operator L with an AD part δL, and rule 258 (L+δL)(u+δu) = L(u) + (δL(u) + L(δu)) + o(δ^2). 259 """ 260 261 def __init__(self,value,coef): 262 """ 263 - value : Some linear operator L 264 - coef : A list of linear operators δL 265 """ 266 self.value = value 267 self.coef = coef 268 269 def __str__(self): return "denseAD_Lin" + str((self.value,self.coef)) 270 def __repr__(self): return "denseAD_Lin" + repr((self.value,self.coef)) 271 272 @property 273 def size_ad(): return len(self.coef) 274 @property 275 def _value(self): return as_callable(self.value) 276 @property 277 def _coef(self): return [as_callable(δL) for δL in self.coef] 278 279 def __call__(self,other): 280 if isinstance(other,denseAD): 281 if other.size_ad==0: return self.__call__(other.value) 282 res = other.apply_linear_operator(self._value) 283 res.coef += np.stack([δL(other.value) for δL in self._coef],axis=-1) 284 return res 285 else: 286 return denseAD(self._value(other), 287 np.stack([δL(other) for δL in self._coef],axis=-1)) 288 289 def __mul__(self,other): return self.__call__(other)
A class implementing a linear operator L with an AD part δL, and rule (L+δL)(u+δu) = L(u) + (δL(u) + L(δu)) + o(δ^2).
def
as_callable(L):
291def as_callable(L): 292 """Make matrices and sparse matrices callable""" 293 if callable(L): return L 294 return lambda x:np.dot(L,x) if isinstance(L,np.ndarray) else (L*x)
Make matrices and sparse matrices callable