agd.Metrics.Seismic.tti
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 copy 6 7from ... import LinearParallel as lp 8from ... import AutomaticDifferentiation as ad 9from ... import FiniteDifferences as fd 10from .. import misc 11from ..riemann import Riemann 12from .implicit_base import ImplicitBase 13from .thomsen_data import ThomsenElasticMaterial, ThomsenGeometricMaterial 14 15 16class TTI(ImplicitBase): 17 """ 18 A family of reduced models, known as Tilted Transversally Anisotropic, 19 and arising in seismic tomography. 20 21 The *dual* unit ball is defined by an equation of the form 22 $$ 23 l(X^2+Y^2,Z^2) + (1/2)*q(X^2+Y^2,Z^2) = 1, 24 $$ 25 where $l$ is linear and $q$ is quadratic, where $X,Y,Z$ are the coefficients 26 of the input vector, usually altered by a linear transformation. 27 In two dimensions, ignore the $Y^2$ term. 28 29 The primal norm is obtained implicitly, by solving an optimization problem. 30 31 Member fields and __init__ arguments : 32 - linear : an array of shape (2,n1,...,nk) encoding the linear part l 33 - quadratic : an array of shape (2,2,n1,...,nk) encoding the quadratic part q 34 - vdim (optional) : the ambient space dimension 35 - *args,**kwargs (optional) : passed to implicit_base 36 """ 37 38 def __init__(self,linear,quadratic,vdim=None,*args,**kwargs): #rotation_angles=None, 39 super(TTI,self).__init__(*args,**kwargs) 40 self.linear = ad.asarray(linear) 41 self.quadratic = ad.asarray(quadratic) 42 assert len(self.linear) == 2 43 assert self.quadratic.shape[:2] == (2,2) 44 self._to_common_field() 45 46 if vdim is None: 47 if self.inverse_transformation is not None: vdim=len(self.inverse_transformation) 48 elif self.linear.ndim>1: vdim = self.linear.ndim-1 49 else: raise ValueError("Unspecified dimension") 50 self._vdim=vdim 51 #self.rotation_angles=rotation_angles 52 53 @property 54 def vdim(self): return self._vdim 55 56 @property 57 def shape(self): return self.linear.shape[1:] 58 59 def _dual_level_root(self,v): 60 """Level set function defining the dual unit ball in the square root domain""" 61 l,q = self._dual_params(v.shape[1:]) 62 return lp.dot_VV(l,v) + 0.5*lp.dot_VAV(v,q,v) - 1. 63 64 def _dual_level(self,v,params=None,relax=0.): 65 """Level set function defining the dual unit ball.""" 66 l,q = self._dual_params(v.shape[1:]) if params is None else params 67 v2 = v**2 68 if self.vdim==3: v2 = ad.array([v2[:2].sum(axis=0),v2[2]]) 69 return lp.dot_VV(l,v2) + 0.5*np.exp(-relax)*lp.dot_VAV(v2,q,v2) - 1. 70 71 def cost_bound(self): 72 return self.Isotropic_approx()[1] 73 # Ignoring the quadratic term for now. 74 return self.Riemann_approx().cost_bound() 75 76 def _dual_params(self,shape=None): 77 return fd.common_field((self.linear,self.quadratic),depths=(1,2),shape=shape) 78 79 def __iter__(self): 80 yield self.linear 81 yield self.quadratic 82 yield self._vdim 83# yield self.rotation_angles 84 for x in super(TTI,self).__iter__(): yield x 85 86 def _to_common_field(self,shape=None): 87 self.linear,self.quadratic,self.inverse_transformation = fd.common_field( 88 (self.linear,self.quadratic,self.inverse_transformation), 89 depths=(1,2,2),shape=shape) 90 91 @classmethod 92 def from_cast(cls,metric): 93 if isinstance(metric,cls): return metric 94 else: raise ValueError("No casting operations supported towards the TTI model") 95 # Even cast from Riemann is non-canonical 96 97 def model_HFM(self): 98 return f"TTI{self.vdim}" 99 100 def extract_xz(self): 101 """ 102 Extract a two dimensional Hooke tensor from a three dimensional one, 103 corresponding to a slice through the X and Z axes. 104 Axes transformation information (rotation) is discarded. 105 """ 106 if len(self.shape)==3: raise ValueError("Three dimensional field") 107 if self.inverse_transformation is not None: 108 raise ValueError("Cannot extract XZ slice from tilted norms") 109 other = copy.copy(self) 110 other._vdim = 2 111 return other 112 113 def flatten(self,transposed_transformation=False,cp_get=False): 114 if self.inverse_transformation is None: 115 xp = ad.cupy_generic.get_array_module(self.linear) 116 trans = fd.as_field(xp.eye(self.vdim,dtype=self.linear.dtype),self.shape,depth=2) 117 else: trans = self.inverse_transformation 118 if transposed_transformation: trans = lp.transpose(lp.inverse(trans)) 119 120 if cp_get: # return a numpy array (memory optimization for GPU intensive) 121 return np.concatenate((self.linear.get(), 122 misc.flatten_symmetric_matrix(self.quadratic.get()), 123 trans.get().reshape((self.vdim**2,)+self.shape)),axis=0) 124 else: 125 return np.concatenate((self.linear,misc.flatten_symmetric_matrix(self.quadratic), 126 trans.reshape((self.vdim**2,)+self.shape)),axis=0) 127 128 @classmethod 129 def expand(cls,arr): 130 vdim = np.sqrt(len(arr)-(2+3)) 131 assert(vdim==int(vdim)) 132 vdim = int(vdim) 133 shape = arr.shape[1:] 134 135 linear = arr[0:2] 136 quadratic = misc.expand_symmetric_matrix(arr[2:5]) 137 inv_trans = arr[5:].reshape((vdim,vdim)+shape) 138 return cls(linear,quadratic,vdim=vdim,inverse_transformation=inv_trans) 139 140 @classmethod 141 def from_hexagonal(cls,c11,_,c13,c33,c44,vdim=3): 142 linear = [c11+c44,c33+c44] 143 mixed = c13**2-c11*c33+2.*c13*c44 144 quadratic = [[-2.*c11*c44,mixed],[mixed,-2.*c33*c44]] 145 return cls(linear,quadratic,vdim=vdim) 146 147 @classmethod 148 def from_ThomsenElastic(cls,tem,vdim=3): 149 """Produces a norm from the given Thomsem elasticity parameters.""" 150 if not isinstance(tem,ThomsenElasticMaterial): tem = ThomsenElasticMaterial(*tem) 151 hex,ρ = tem.to_hexagonal() 152 return cls.from_hexagonal(*hex,vdim),ρ 153 154 @classmethod 155 def from_ThomsenGeometric(cls,tgm,vdim=3,normalize_Vp=False): 156 """Produces a norm from the given Thomsen geometric paramters.""" 157 if not isinstance(tgm,ThomsenGeometricMaterial): tgm = ThomsenGeometricMaterial(*tgm) 158 if normalize_Vp: 159 Vp,Vs,ϵ,δ = tgm 160 tgm = ThomsenGeometricMaterial(1.,Vs/Vp,ϵ,δ) 161 c11,c13,c33,c44 = tgm.to_c() 162 return cls.from_hexagonal(c11,None,c13,c33,c44,vdim=vdim) 163 164 def α_bounds(self): 165 """ 166 The TTI norm can be written as an enveloppe of ellipses, with eigenvalues 167 (1-α,α) / μ(α) or (1-α,1-α,α)/μ(α), where α is within the bounds given by this function. 168 Note : another way to obtain the envelope is to use the Riemann_envelope method. 169 170 Returns : αa,αb,mix_is_min 171 """ 172 l,q = self.linear,self._q() 173 a,b = _axes_intersections(l,q) 174 z = np.zeros_like(a) 175 ga = _grad_ratio(l,q,(a,z)) 176 gb = _grad_ratio(l,q,(z,b)) 177 αa = ga[1]/ga.sum(axis=0) 178 αb = gb[1]/gb.sum(axis=0) 179 return np.minimum(αa,αb),np.maximum(αa,αb),αa<αb 180 181 def μ(self,α): 182 """ See the method α_bounds. """ 183 a = ad.array((1-α,α)) 184 l = self.linear 185 Q = self.quadratic 186 δ = lp.det(Q) 187 δRef = np.sum(Q**2,axis=(0,1)) # If δ/δRef is small, then degenerate case (two lines) 188 R = ad.array([[Q[1,1],-Q[0,1]],[-Q[1,0],Q[0,0]]]) # Adjugate of Q 189 Rl = lp.dot_AV(R,l) 190 s = 2*δ+lp.dot_VV(l,Rl) 191 ε = np.sign(s) 192 aRa = lp.dot_VAV(a,R,a) 193 194 # For generic parameters, the result is num/den. However, den = num = 0 is possible. 195 # This arises in particular if Q = λ l l^T, in which case the conic consists of two lines. 196 # We must also check wether Q is close to zero, in which case the conic degenerates 197 # to a line. In both these cases, we approximate the conic piece witha line, and 198 # we get μ by using one intersection along an axis : satisfy (1-α,α).(a0,0)/μ(α) = 1. 199 # Another possible criterion, more costly but possibly better, would be to directly 200 # check the difference between self.α_bounds() - if they are close, then the conic piece 201 # can be well approximated with a line. 202 tol = 5*np.finfo(Q.dtype).eps; 203 degen = np.sum(Rl**2,axis=0)<=tol*np.sum(l**2,axis=0)**3 # conic piece degenerates to a line 204# print(np.sum(Rl**2,axis=0)/np.sum(l**2,axis=0)**3,tol) 205# tol = 100*np.finfo(Q.dtype).eps; degen = np.abs(δ)<=δRef*tol # Degenerate quadratic form => two lines 206# print(degen,l,R,Rl) 207 sol_degen = (1-α) * _solve2_above(-2,l[0],Q[0,0],0.) # Use intersection along one axis 208 209 num = lp.det([a,l])**2+2*aRa 210 den = ε*np.sqrt(aRa*s)+lp.dot_VV(a,Rl) 211# print(δ/δRef,np.finfo(Q.dtype).eps) 212# print(self.α_bounds(),α) 213# print(degen,sol_degen,num,den) 214 return np.where(degen,sol_degen, num/den) 215 216 def Isotropic_approx(self): 217 """ 218 Isotropic approximation of the TTI norm. 219 Returns : (cmin,cmax). These costs correspond to the interior and exterior approximations. 220 Assumption : the linear transformation must be a rotation. 221 """ 222 αmin,αmax,mix_is_min = self.α_bounds() 223 l,q = self.linear,self._q() 224 a,b = _axes_intersections(l,q) 225 # Costs corresponding to vertical and horizontal propagation for the Dual norm 226 c0,c1=np.sqrt(1/a),np.sqrt(1/b) 227 pos = (αmin<0.5) & (0.5<αmax) # If pos==True, one extremal speed is not on the axes. 228 αmed = np.where(pos,0.5,(αmin+αmax)/2) 229 # 0.5 corresponds to the isotropy. (αmin+αmax)/2 is a dummy value in [αmin,αmax] 230 ch = np.where(pos,np.sqrt(0.5/self.μ(αmed)),(c0+c1)/2) 231 c = np.sort(ad.asarray([c0,ch,c1]),axis=0) 232 return 1/c[2],1/c[0] # Take inverses, to account for norm duality 233 234 def Riemann_approx(self,avg=False,**kwargs): 235 """ 236 Riemannian approximations of the TTI norm, homothetic to each other, and expectedly good. 237 Returns : 238 - Mmin,Mmax. Some interior and exterior approximating Riemannian metrics. 239 - Mavg, if avg=True. A good approximating Riemannian metric, neither interior nor exterior. 240 - kwargs : passed to Riemann.dual (typical : avoid_np_linalg_inv=True, for speed) 241 """ 242 import time; top = time.time() 243 l,q = self.linear,self._q() 244 a,b = _axes_intersections(l,q) 245 ai,bi = 1/a,1/b 246 α = bi/(ai+bi) 247 c = 1/(self.μ(α)*(ai+bi)) 248 cmin,cmax,cavg = np.minimum(1,c),np.maximum(1,c),((1+np.sqrt(c))/2)**2 249 diag = (ai,bi) if self.vdim==2 else (ai,ai,bi) 250 riem = Riemann.from_diagonal(diag) 251 if self.inverse_transformation is not None: 252 riem = riem.inv_transform(self.inverse_transformation) 253 m = riem.dual(**kwargs).m # Take inverse, to account for norm duality 254 return m/cavg if avg else (m/cmax,m/cmin) 255 256 def Riemann_envelope(self,nmix,gpu=False): 257 """ 258 Approximation of a TTI norm using an envelope of Riemannian norms. 259 - nmix : number of ellipses used for the approximation. 260 - gpu : same implementation as on the GPU (changes almost nothing) 261 returns 262 - riems : a list of nmix Riemannian norms. 263 - mix_is_min : wether to take the minimum or the maximum of the Riemannian norms. 264 """ 265 # Set the interpolation times 266 if isinstance(nmix,int): 267 if nmix==1: ts=[0.5] 268 else: ts = np.linspace(0,1,nmix) 269 else: ts = nmix # Hack to get specific interpolation times, must be sorted 270 271 if gpu: 272 l,q = self.linear,self._q() 273 ab = _axes_intersections(l,q) 274 diags = _diags(l,q,ts,ab) 275 mix_is_min = lp.det([diags[0],diags[-1]])>0 if len(diags) else None 276 else: 277 αmin,αmax,mix_is_min = self.α_bounds() 278 diags = [np.array([1-α,α])/self.μ(α) for α in np.linspace(αmin,αmax,nmix)] 279 280 if self.vdim==3: diags = [(a,a,b) for a,b in diags] 281 riems = [Riemann.from_diagonal(1./ad.array(diag)) for diag in diags] 282 if self.inverse_transformation is not None: 283 riems = [riem.inv_transform(self.inverse_transformation) for riem in riems] 284 285 return mix_is_min, riems 286 287 def _q(self): 288 """ 289 Quadratic part, in format compatible with the C routines adapted below 290 """ 291 return self.quadratic[((0,0,1),(0,1,1))] 292 293# ----- The following code is adapted from agd/Eikonal/HFM_CUDA/cuda/TTI_.h ----- 294# It computes the approximation of the TTI norm with an envelope of riemannian norms 295# The chosen collection of ellipses (very slightly) differs from a uniform sampling 296# within αmin,αmax given by self.α_bounds() 297 298def _solve2(a,b,c): 299 """ 300 Returns the two roots of a quadratic equation, a + 2 b t + c t^2. 301 The discriminant must be non-negative, but aside from that the equation may be degenerate. 302 """ 303 sdelta = np.sqrt(b*b-a*c); 304 u = -b + sdelta 305 v = -b - sdelta 306 b0 = np.abs(c)>np.abs(a) 307 xp = ad.cupy_generic.get_array_module(b0) 308 b1 = xp.asarray(a!=0) # Needed with cupy 309# with np.errstate(divide='ignore'): # Does not silent much 310 return xp.asarray( (np.where(b0,u/c,np.where(b1,a/u,0.)), 311 np.where(b0,v/c,np.where(b1,a/v,np.inf))) ) 312 313def _solve2_above(a, b, c, above): 314 """ 315 Returns the smallest root of the considered quadratic equation above the given threshold. 316 Such a root is assumed to exist. 317 """ 318 r = np.sort(_solve2(a,b,c),axis=0) 319 return np.where(r[0]>=above, r[0], r[1]) 320 321def _axes_intersections(l,q): 322 """ 323 Finds the intersections (a,0) and (0,b) of the curve f(x)=0 324 with the axes (the closest intersections to the origin). 325 """ 326 return _solve2_above(-2,l[0],q[0],0.), _solve2_above(-2,l[1],q[2],0.) 327 328def _grad_ratio(l,q,x): 329 """ 330 Returns g(x) := df(x)/<x,df(x)> where f(x):= C + 2 <l,x> + <qx,x> 331 Note that the curve tangent to the level line of f at x is 332 <y-x,df(x)> ≥ 0, equivalently <y,g(x)> ≥ 1 333 """ 334 hgrad = ad.array([q[0]*x[0]+q[1]*x[1]+l[0], q[1]*x[0]+q[2]*x[1]+l[1]]) #lp.dot_AV(q,x)+l # df(x)/2 335 return hgrad/lp.dot_VV(x,hgrad) 336 337def _diags(l, q, ts, axes_intersections): 338 """ 339 Samples the curve defined by {x≥0|f(x)=0}, 340 (the connected component closest to the origin) 341 where f(x):= -2 + 2 <l,x> + <qx,x>, 342 and returns diag(i) := grad f(x)/<x,grad f(x)>. 343 """ 344 a,b=axes_intersections 345 zero = np.zeros_like(a) 346 347 # Change of basis in f, with e0 = {1/2.,1/2.}, e1 = {1/2.,-1/2.} 348 L = ad.array([l[0]+l[1], l[0]-l[1]])/2. 349 Q = ad.array([q[0]+2*q[1]+q[2], q[0]-q[2], q[0]-2*q[1]+q[2]])/4. 350 351 diags=[] 352 for t in ts: 353 if t==0.: x=(a,zero) 354 elif t==1.: x=(zero,b) 355 else : 356 v = (1.-t)*a - t*b 357 # Solving f(u e0+ v e_1) = 0 w.r.t u 358 u = _solve2_above(-2.+2.*L[1]*v+Q[2]*v*v, L[0]+Q[1]*v, Q[0], np.abs(v)) 359 # Inverse change of basis 360 x = ad.array([u+v, u-v])/2. 361 diags.append(_grad_ratio(l,q,x)) 362 363 return diags 364 365 366# ---- Some instances of TTI metrics ---- 367 368# See Hooke.py file for reference 369TTI.mica = TTI.from_hexagonal(178.,42.4,14.5,54.9,12.2), 2.79 370# Stishovite is tetragonal, not hexagonal, but the P-Wave velocity, in the XZ plane, 371# is equivalent to an hexagonal model. 372TTI.stishovite2 = TTI.from_hexagonal(453,np.nan,203,776,252).extract_xz(), 4.29
17class TTI(ImplicitBase): 18 """ 19 A family of reduced models, known as Tilted Transversally Anisotropic, 20 and arising in seismic tomography. 21 22 The *dual* unit ball is defined by an equation of the form 23 $$ 24 l(X^2+Y^2,Z^2) + (1/2)*q(X^2+Y^2,Z^2) = 1, 25 $$ 26 where $l$ is linear and $q$ is quadratic, where $X,Y,Z$ are the coefficients 27 of the input vector, usually altered by a linear transformation. 28 In two dimensions, ignore the $Y^2$ term. 29 30 The primal norm is obtained implicitly, by solving an optimization problem. 31 32 Member fields and __init__ arguments : 33 - linear : an array of shape (2,n1,...,nk) encoding the linear part l 34 - quadratic : an array of shape (2,2,n1,...,nk) encoding the quadratic part q 35 - vdim (optional) : the ambient space dimension 36 - *args,**kwargs (optional) : passed to implicit_base 37 """ 38 39 def __init__(self,linear,quadratic,vdim=None,*args,**kwargs): #rotation_angles=None, 40 super(TTI,self).__init__(*args,**kwargs) 41 self.linear = ad.asarray(linear) 42 self.quadratic = ad.asarray(quadratic) 43 assert len(self.linear) == 2 44 assert self.quadratic.shape[:2] == (2,2) 45 self._to_common_field() 46 47 if vdim is None: 48 if self.inverse_transformation is not None: vdim=len(self.inverse_transformation) 49 elif self.linear.ndim>1: vdim = self.linear.ndim-1 50 else: raise ValueError("Unspecified dimension") 51 self._vdim=vdim 52 #self.rotation_angles=rotation_angles 53 54 @property 55 def vdim(self): return self._vdim 56 57 @property 58 def shape(self): return self.linear.shape[1:] 59 60 def _dual_level_root(self,v): 61 """Level set function defining the dual unit ball in the square root domain""" 62 l,q = self._dual_params(v.shape[1:]) 63 return lp.dot_VV(l,v) + 0.5*lp.dot_VAV(v,q,v) - 1. 64 65 def _dual_level(self,v,params=None,relax=0.): 66 """Level set function defining the dual unit ball.""" 67 l,q = self._dual_params(v.shape[1:]) if params is None else params 68 v2 = v**2 69 if self.vdim==3: v2 = ad.array([v2[:2].sum(axis=0),v2[2]]) 70 return lp.dot_VV(l,v2) + 0.5*np.exp(-relax)*lp.dot_VAV(v2,q,v2) - 1. 71 72 def cost_bound(self): 73 return self.Isotropic_approx()[1] 74 # Ignoring the quadratic term for now. 75 return self.Riemann_approx().cost_bound() 76 77 def _dual_params(self,shape=None): 78 return fd.common_field((self.linear,self.quadratic),depths=(1,2),shape=shape) 79 80 def __iter__(self): 81 yield self.linear 82 yield self.quadratic 83 yield self._vdim 84# yield self.rotation_angles 85 for x in super(TTI,self).__iter__(): yield x 86 87 def _to_common_field(self,shape=None): 88 self.linear,self.quadratic,self.inverse_transformation = fd.common_field( 89 (self.linear,self.quadratic,self.inverse_transformation), 90 depths=(1,2,2),shape=shape) 91 92 @classmethod 93 def from_cast(cls,metric): 94 if isinstance(metric,cls): return metric 95 else: raise ValueError("No casting operations supported towards the TTI model") 96 # Even cast from Riemann is non-canonical 97 98 def model_HFM(self): 99 return f"TTI{self.vdim}" 100 101 def extract_xz(self): 102 """ 103 Extract a two dimensional Hooke tensor from a three dimensional one, 104 corresponding to a slice through the X and Z axes. 105 Axes transformation information (rotation) is discarded. 106 """ 107 if len(self.shape)==3: raise ValueError("Three dimensional field") 108 if self.inverse_transformation is not None: 109 raise ValueError("Cannot extract XZ slice from tilted norms") 110 other = copy.copy(self) 111 other._vdim = 2 112 return other 113 114 def flatten(self,transposed_transformation=False,cp_get=False): 115 if self.inverse_transformation is None: 116 xp = ad.cupy_generic.get_array_module(self.linear) 117 trans = fd.as_field(xp.eye(self.vdim,dtype=self.linear.dtype),self.shape,depth=2) 118 else: trans = self.inverse_transformation 119 if transposed_transformation: trans = lp.transpose(lp.inverse(trans)) 120 121 if cp_get: # return a numpy array (memory optimization for GPU intensive) 122 return np.concatenate((self.linear.get(), 123 misc.flatten_symmetric_matrix(self.quadratic.get()), 124 trans.get().reshape((self.vdim**2,)+self.shape)),axis=0) 125 else: 126 return np.concatenate((self.linear,misc.flatten_symmetric_matrix(self.quadratic), 127 trans.reshape((self.vdim**2,)+self.shape)),axis=0) 128 129 @classmethod 130 def expand(cls,arr): 131 vdim = np.sqrt(len(arr)-(2+3)) 132 assert(vdim==int(vdim)) 133 vdim = int(vdim) 134 shape = arr.shape[1:] 135 136 linear = arr[0:2] 137 quadratic = misc.expand_symmetric_matrix(arr[2:5]) 138 inv_trans = arr[5:].reshape((vdim,vdim)+shape) 139 return cls(linear,quadratic,vdim=vdim,inverse_transformation=inv_trans) 140 141 @classmethod 142 def from_hexagonal(cls,c11,_,c13,c33,c44,vdim=3): 143 linear = [c11+c44,c33+c44] 144 mixed = c13**2-c11*c33+2.*c13*c44 145 quadratic = [[-2.*c11*c44,mixed],[mixed,-2.*c33*c44]] 146 return cls(linear,quadratic,vdim=vdim) 147 148 @classmethod 149 def from_ThomsenElastic(cls,tem,vdim=3): 150 """Produces a norm from the given Thomsem elasticity parameters.""" 151 if not isinstance(tem,ThomsenElasticMaterial): tem = ThomsenElasticMaterial(*tem) 152 hex,ρ = tem.to_hexagonal() 153 return cls.from_hexagonal(*hex,vdim),ρ 154 155 @classmethod 156 def from_ThomsenGeometric(cls,tgm,vdim=3,normalize_Vp=False): 157 """Produces a norm from the given Thomsen geometric paramters.""" 158 if not isinstance(tgm,ThomsenGeometricMaterial): tgm = ThomsenGeometricMaterial(*tgm) 159 if normalize_Vp: 160 Vp,Vs,ϵ,δ = tgm 161 tgm = ThomsenGeometricMaterial(1.,Vs/Vp,ϵ,δ) 162 c11,c13,c33,c44 = tgm.to_c() 163 return cls.from_hexagonal(c11,None,c13,c33,c44,vdim=vdim) 164 165 def α_bounds(self): 166 """ 167 The TTI norm can be written as an enveloppe of ellipses, with eigenvalues 168 (1-α,α) / μ(α) or (1-α,1-α,α)/μ(α), where α is within the bounds given by this function. 169 Note : another way to obtain the envelope is to use the Riemann_envelope method. 170 171 Returns : αa,αb,mix_is_min 172 """ 173 l,q = self.linear,self._q() 174 a,b = _axes_intersections(l,q) 175 z = np.zeros_like(a) 176 ga = _grad_ratio(l,q,(a,z)) 177 gb = _grad_ratio(l,q,(z,b)) 178 αa = ga[1]/ga.sum(axis=0) 179 αb = gb[1]/gb.sum(axis=0) 180 return np.minimum(αa,αb),np.maximum(αa,αb),αa<αb 181 182 def μ(self,α): 183 """ See the method α_bounds. """ 184 a = ad.array((1-α,α)) 185 l = self.linear 186 Q = self.quadratic 187 δ = lp.det(Q) 188 δRef = np.sum(Q**2,axis=(0,1)) # If δ/δRef is small, then degenerate case (two lines) 189 R = ad.array([[Q[1,1],-Q[0,1]],[-Q[1,0],Q[0,0]]]) # Adjugate of Q 190 Rl = lp.dot_AV(R,l) 191 s = 2*δ+lp.dot_VV(l,Rl) 192 ε = np.sign(s) 193 aRa = lp.dot_VAV(a,R,a) 194 195 # For generic parameters, the result is num/den. However, den = num = 0 is possible. 196 # This arises in particular if Q = λ l l^T, in which case the conic consists of two lines. 197 # We must also check wether Q is close to zero, in which case the conic degenerates 198 # to a line. In both these cases, we approximate the conic piece witha line, and 199 # we get μ by using one intersection along an axis : satisfy (1-α,α).(a0,0)/μ(α) = 1. 200 # Another possible criterion, more costly but possibly better, would be to directly 201 # check the difference between self.α_bounds() - if they are close, then the conic piece 202 # can be well approximated with a line. 203 tol = 5*np.finfo(Q.dtype).eps; 204 degen = np.sum(Rl**2,axis=0)<=tol*np.sum(l**2,axis=0)**3 # conic piece degenerates to a line 205# print(np.sum(Rl**2,axis=0)/np.sum(l**2,axis=0)**3,tol) 206# tol = 100*np.finfo(Q.dtype).eps; degen = np.abs(δ)<=δRef*tol # Degenerate quadratic form => two lines 207# print(degen,l,R,Rl) 208 sol_degen = (1-α) * _solve2_above(-2,l[0],Q[0,0],0.) # Use intersection along one axis 209 210 num = lp.det([a,l])**2+2*aRa 211 den = ε*np.sqrt(aRa*s)+lp.dot_VV(a,Rl) 212# print(δ/δRef,np.finfo(Q.dtype).eps) 213# print(self.α_bounds(),α) 214# print(degen,sol_degen,num,den) 215 return np.where(degen,sol_degen, num/den) 216 217 def Isotropic_approx(self): 218 """ 219 Isotropic approximation of the TTI norm. 220 Returns : (cmin,cmax). These costs correspond to the interior and exterior approximations. 221 Assumption : the linear transformation must be a rotation. 222 """ 223 αmin,αmax,mix_is_min = self.α_bounds() 224 l,q = self.linear,self._q() 225 a,b = _axes_intersections(l,q) 226 # Costs corresponding to vertical and horizontal propagation for the Dual norm 227 c0,c1=np.sqrt(1/a),np.sqrt(1/b) 228 pos = (αmin<0.5) & (0.5<αmax) # If pos==True, one extremal speed is not on the axes. 229 αmed = np.where(pos,0.5,(αmin+αmax)/2) 230 # 0.5 corresponds to the isotropy. (αmin+αmax)/2 is a dummy value in [αmin,αmax] 231 ch = np.where(pos,np.sqrt(0.5/self.μ(αmed)),(c0+c1)/2) 232 c = np.sort(ad.asarray([c0,ch,c1]),axis=0) 233 return 1/c[2],1/c[0] # Take inverses, to account for norm duality 234 235 def Riemann_approx(self,avg=False,**kwargs): 236 """ 237 Riemannian approximations of the TTI norm, homothetic to each other, and expectedly good. 238 Returns : 239 - Mmin,Mmax. Some interior and exterior approximating Riemannian metrics. 240 - Mavg, if avg=True. A good approximating Riemannian metric, neither interior nor exterior. 241 - kwargs : passed to Riemann.dual (typical : avoid_np_linalg_inv=True, for speed) 242 """ 243 import time; top = time.time() 244 l,q = self.linear,self._q() 245 a,b = _axes_intersections(l,q) 246 ai,bi = 1/a,1/b 247 α = bi/(ai+bi) 248 c = 1/(self.μ(α)*(ai+bi)) 249 cmin,cmax,cavg = np.minimum(1,c),np.maximum(1,c),((1+np.sqrt(c))/2)**2 250 diag = (ai,bi) if self.vdim==2 else (ai,ai,bi) 251 riem = Riemann.from_diagonal(diag) 252 if self.inverse_transformation is not None: 253 riem = riem.inv_transform(self.inverse_transformation) 254 m = riem.dual(**kwargs).m # Take inverse, to account for norm duality 255 return m/cavg if avg else (m/cmax,m/cmin) 256 257 def Riemann_envelope(self,nmix,gpu=False): 258 """ 259 Approximation of a TTI norm using an envelope of Riemannian norms. 260 - nmix : number of ellipses used for the approximation. 261 - gpu : same implementation as on the GPU (changes almost nothing) 262 returns 263 - riems : a list of nmix Riemannian norms. 264 - mix_is_min : wether to take the minimum or the maximum of the Riemannian norms. 265 """ 266 # Set the interpolation times 267 if isinstance(nmix,int): 268 if nmix==1: ts=[0.5] 269 else: ts = np.linspace(0,1,nmix) 270 else: ts = nmix # Hack to get specific interpolation times, must be sorted 271 272 if gpu: 273 l,q = self.linear,self._q() 274 ab = _axes_intersections(l,q) 275 diags = _diags(l,q,ts,ab) 276 mix_is_min = lp.det([diags[0],diags[-1]])>0 if len(diags) else None 277 else: 278 αmin,αmax,mix_is_min = self.α_bounds() 279 diags = [np.array([1-α,α])/self.μ(α) for α in np.linspace(αmin,αmax,nmix)] 280 281 if self.vdim==3: diags = [(a,a,b) for a,b in diags] 282 riems = [Riemann.from_diagonal(1./ad.array(diag)) for diag in diags] 283 if self.inverse_transformation is not None: 284 riems = [riem.inv_transform(self.inverse_transformation) for riem in riems] 285 286 return mix_is_min, riems 287 288 def _q(self): 289 """ 290 Quadratic part, in format compatible with the C routines adapted below 291 """ 292 return self.quadratic[((0,0,1),(0,1,1))]
A family of reduced models, known as Tilted Transversally Anisotropic, and arising in seismic tomography.
The dual unit ball is defined by an equation of the form $$ l(X^2+Y^2,Z^2) + (1/2)*q(X^2+Y^2,Z^2) = 1, $$ where $l$ is linear and $q$ is quadratic, where $X,Y,Z$ are the coefficients of the input vector, usually altered by a linear transformation. In two dimensions, ignore the $Y^2$ term.
The primal norm is obtained implicitly, by solving an optimization problem.
Member fields and __init__ arguments :
- linear : an array of shape (2,n1,...,nk) encoding the linear part l
- quadratic : an array of shape (2,2,n1,...,nk) encoding the quadratic part q
- vdim (optional) : the ambient space dimension
- args,*kwargs (optional) : passed to implicit_base
39 def __init__(self,linear,quadratic,vdim=None,*args,**kwargs): #rotation_angles=None, 40 super(TTI,self).__init__(*args,**kwargs) 41 self.linear = ad.asarray(linear) 42 self.quadratic = ad.asarray(quadratic) 43 assert len(self.linear) == 2 44 assert self.quadratic.shape[:2] == (2,2) 45 self._to_common_field() 46 47 if vdim is None: 48 if self.inverse_transformation is not None: vdim=len(self.inverse_transformation) 49 elif self.linear.ndim>1: vdim = self.linear.ndim-1 50 else: raise ValueError("Unspecified dimension") 51 self._vdim=vdim 52 #self.rotation_angles=rotation_angles
Dimensions of the underlying domain.
Expected to be the empty tuple, or a tuple of length vdim
.
72 def cost_bound(self): 73 return self.Isotropic_approx()[1] 74 # Ignoring the quadratic term for now. 75 return self.Riemann_approx().cost_bound()
Upper bound on $N(u)$, for any unit vector $u$, where $N$ is the norm
defined by the class.
92 @classmethod 93 def from_cast(cls,metric): 94 if isinstance(metric,cls): return metric 95 else: raise ValueError("No casting operations supported towards the TTI model") 96 # Even cast from Riemann is non-canonical
Produces a metric by casting another metric of a compatible type.
101 def extract_xz(self): 102 """ 103 Extract a two dimensional Hooke tensor from a three dimensional one, 104 corresponding to a slice through the X and Z axes. 105 Axes transformation information (rotation) is discarded. 106 """ 107 if len(self.shape)==3: raise ValueError("Three dimensional field") 108 if self.inverse_transformation is not None: 109 raise ValueError("Cannot extract XZ slice from tilted norms") 110 other = copy.copy(self) 111 other._vdim = 2 112 return other
Extract a two dimensional Hooke tensor from a three dimensional one, corresponding to a slice through the X and Z axes. Axes transformation information (rotation) is discarded.
114 def flatten(self,transposed_transformation=False,cp_get=False): 115 if self.inverse_transformation is None: 116 xp = ad.cupy_generic.get_array_module(self.linear) 117 trans = fd.as_field(xp.eye(self.vdim,dtype=self.linear.dtype),self.shape,depth=2) 118 else: trans = self.inverse_transformation 119 if transposed_transformation: trans = lp.transpose(lp.inverse(trans)) 120 121 if cp_get: # return a numpy array (memory optimization for GPU intensive) 122 return np.concatenate((self.linear.get(), 123 misc.flatten_symmetric_matrix(self.quadratic.get()), 124 trans.get().reshape((self.vdim**2,)+self.shape)),axis=0) 125 else: 126 return np.concatenate((self.linear,misc.flatten_symmetric_matrix(self.quadratic), 127 trans.reshape((self.vdim**2,)+self.shape)),axis=0)
Flattens and concatenate the member fields into a single array.
129 @classmethod 130 def expand(cls,arr): 131 vdim = np.sqrt(len(arr)-(2+3)) 132 assert(vdim==int(vdim)) 133 vdim = int(vdim) 134 shape = arr.shape[1:] 135 136 linear = arr[0:2] 137 quadratic = misc.expand_symmetric_matrix(arr[2:5]) 138 inv_trans = arr[5:].reshape((vdim,vdim)+shape) 139 return cls(linear,quadratic,vdim=vdim,inverse_transformation=inv_trans)
Inverse of the flatten member function. Turns a suitable array into a metric.
148 @classmethod 149 def from_ThomsenElastic(cls,tem,vdim=3): 150 """Produces a norm from the given Thomsem elasticity parameters.""" 151 if not isinstance(tem,ThomsenElasticMaterial): tem = ThomsenElasticMaterial(*tem) 152 hex,ρ = tem.to_hexagonal() 153 return cls.from_hexagonal(*hex,vdim),ρ
Produces a norm from the given Thomsem elasticity parameters.
155 @classmethod 156 def from_ThomsenGeometric(cls,tgm,vdim=3,normalize_Vp=False): 157 """Produces a norm from the given Thomsen geometric paramters.""" 158 if not isinstance(tgm,ThomsenGeometricMaterial): tgm = ThomsenGeometricMaterial(*tgm) 159 if normalize_Vp: 160 Vp,Vs,ϵ,δ = tgm 161 tgm = ThomsenGeometricMaterial(1.,Vs/Vp,ϵ,δ) 162 c11,c13,c33,c44 = tgm.to_c() 163 return cls.from_hexagonal(c11,None,c13,c33,c44,vdim=vdim)
Produces a norm from the given Thomsen geometric paramters.
165 def α_bounds(self): 166 """ 167 The TTI norm can be written as an enveloppe of ellipses, with eigenvalues 168 (1-α,α) / μ(α) or (1-α,1-α,α)/μ(α), where α is within the bounds given by this function. 169 Note : another way to obtain the envelope is to use the Riemann_envelope method. 170 171 Returns : αa,αb,mix_is_min 172 """ 173 l,q = self.linear,self._q() 174 a,b = _axes_intersections(l,q) 175 z = np.zeros_like(a) 176 ga = _grad_ratio(l,q,(a,z)) 177 gb = _grad_ratio(l,q,(z,b)) 178 αa = ga[1]/ga.sum(axis=0) 179 αb = gb[1]/gb.sum(axis=0) 180 return np.minimum(αa,αb),np.maximum(αa,αb),αa<αb
The TTI norm can be written as an enveloppe of ellipses, with eigenvalues (1-α,α) / μ(α) or (1-α,1-α,α)/μ(α), where α is within the bounds given by this function. Note : another way to obtain the envelope is to use the Riemann_envelope method.
Returns : αa,αb,mix_is_min
182 def μ(self,α): 183 """ See the method α_bounds. """ 184 a = ad.array((1-α,α)) 185 l = self.linear 186 Q = self.quadratic 187 δ = lp.det(Q) 188 δRef = np.sum(Q**2,axis=(0,1)) # If δ/δRef is small, then degenerate case (two lines) 189 R = ad.array([[Q[1,1],-Q[0,1]],[-Q[1,0],Q[0,0]]]) # Adjugate of Q 190 Rl = lp.dot_AV(R,l) 191 s = 2*δ+lp.dot_VV(l,Rl) 192 ε = np.sign(s) 193 aRa = lp.dot_VAV(a,R,a) 194 195 # For generic parameters, the result is num/den. However, den = num = 0 is possible. 196 # This arises in particular if Q = λ l l^T, in which case the conic consists of two lines. 197 # We must also check wether Q is close to zero, in which case the conic degenerates 198 # to a line. In both these cases, we approximate the conic piece witha line, and 199 # we get μ by using one intersection along an axis : satisfy (1-α,α).(a0,0)/μ(α) = 1. 200 # Another possible criterion, more costly but possibly better, would be to directly 201 # check the difference between self.α_bounds() - if they are close, then the conic piece 202 # can be well approximated with a line. 203 tol = 5*np.finfo(Q.dtype).eps; 204 degen = np.sum(Rl**2,axis=0)<=tol*np.sum(l**2,axis=0)**3 # conic piece degenerates to a line 205# print(np.sum(Rl**2,axis=0)/np.sum(l**2,axis=0)**3,tol) 206# tol = 100*np.finfo(Q.dtype).eps; degen = np.abs(δ)<=δRef*tol # Degenerate quadratic form => two lines 207# print(degen,l,R,Rl) 208 sol_degen = (1-α) * _solve2_above(-2,l[0],Q[0,0],0.) # Use intersection along one axis 209 210 num = lp.det([a,l])**2+2*aRa 211 den = ε*np.sqrt(aRa*s)+lp.dot_VV(a,Rl) 212# print(δ/δRef,np.finfo(Q.dtype).eps) 213# print(self.α_bounds(),α) 214# print(degen,sol_degen,num,den) 215 return np.where(degen,sol_degen, num/den)
See the method α_bounds.
217 def Isotropic_approx(self): 218 """ 219 Isotropic approximation of the TTI norm. 220 Returns : (cmin,cmax). These costs correspond to the interior and exterior approximations. 221 Assumption : the linear transformation must be a rotation. 222 """ 223 αmin,αmax,mix_is_min = self.α_bounds() 224 l,q = self.linear,self._q() 225 a,b = _axes_intersections(l,q) 226 # Costs corresponding to vertical and horizontal propagation for the Dual norm 227 c0,c1=np.sqrt(1/a),np.sqrt(1/b) 228 pos = (αmin<0.5) & (0.5<αmax) # If pos==True, one extremal speed is not on the axes. 229 αmed = np.where(pos,0.5,(αmin+αmax)/2) 230 # 0.5 corresponds to the isotropy. (αmin+αmax)/2 is a dummy value in [αmin,αmax] 231 ch = np.where(pos,np.sqrt(0.5/self.μ(αmed)),(c0+c1)/2) 232 c = np.sort(ad.asarray([c0,ch,c1]),axis=0) 233 return 1/c[2],1/c[0] # Take inverses, to account for norm duality
Isotropic approximation of the TTI norm. Returns : (cmin,cmax). These costs correspond to the interior and exterior approximations. Assumption : the linear transformation must be a rotation.
235 def Riemann_approx(self,avg=False,**kwargs): 236 """ 237 Riemannian approximations of the TTI norm, homothetic to each other, and expectedly good. 238 Returns : 239 - Mmin,Mmax. Some interior and exterior approximating Riemannian metrics. 240 - Mavg, if avg=True. A good approximating Riemannian metric, neither interior nor exterior. 241 - kwargs : passed to Riemann.dual (typical : avoid_np_linalg_inv=True, for speed) 242 """ 243 import time; top = time.time() 244 l,q = self.linear,self._q() 245 a,b = _axes_intersections(l,q) 246 ai,bi = 1/a,1/b 247 α = bi/(ai+bi) 248 c = 1/(self.μ(α)*(ai+bi)) 249 cmin,cmax,cavg = np.minimum(1,c),np.maximum(1,c),((1+np.sqrt(c))/2)**2 250 diag = (ai,bi) if self.vdim==2 else (ai,ai,bi) 251 riem = Riemann.from_diagonal(diag) 252 if self.inverse_transformation is not None: 253 riem = riem.inv_transform(self.inverse_transformation) 254 m = riem.dual(**kwargs).m # Take inverse, to account for norm duality 255 return m/cavg if avg else (m/cmax,m/cmin)
Riemannian approximations of the TTI norm, homothetic to each other, and expectedly good. Returns :
- Mmin,Mmax. Some interior and exterior approximating Riemannian metrics.
- Mavg, if avg=True. A good approximating Riemannian metric, neither interior nor exterior.
- kwargs : passed to Riemann.dual (typical : avoid_np_linalg_inv=True, for speed)
257 def Riemann_envelope(self,nmix,gpu=False): 258 """ 259 Approximation of a TTI norm using an envelope of Riemannian norms. 260 - nmix : number of ellipses used for the approximation. 261 - gpu : same implementation as on the GPU (changes almost nothing) 262 returns 263 - riems : a list of nmix Riemannian norms. 264 - mix_is_min : wether to take the minimum or the maximum of the Riemannian norms. 265 """ 266 # Set the interpolation times 267 if isinstance(nmix,int): 268 if nmix==1: ts=[0.5] 269 else: ts = np.linspace(0,1,nmix) 270 else: ts = nmix # Hack to get specific interpolation times, must be sorted 271 272 if gpu: 273 l,q = self.linear,self._q() 274 ab = _axes_intersections(l,q) 275 diags = _diags(l,q,ts,ab) 276 mix_is_min = lp.det([diags[0],diags[-1]])>0 if len(diags) else None 277 else: 278 αmin,αmax,mix_is_min = self.α_bounds() 279 diags = [np.array([1-α,α])/self.μ(α) for α in np.linspace(αmin,αmax,nmix)] 280 281 if self.vdim==3: diags = [(a,a,b) for a,b in diags] 282 riems = [Riemann.from_diagonal(1./ad.array(diag)) for diag in diags] 283 if self.inverse_transformation is not None: 284 riems = [riem.inv_transform(self.inverse_transformation) for riem in riems] 285 286 return mix_is_min, riems
Approximation of a TTI norm using an envelope of Riemannian norms.
- nmix : number of ellipses used for the approximation.
- gpu : same implementation as on the GPU (changes almost nothing) returns
- riems : a list of nmix Riemannian norms.
- mix_is_min : wether to take the minimum or the maximum of the Riemannian norms.