agd.Metrics.Seismic.hooke

  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 itertools
  6import copy
  7
  8from ... import AutomaticDifferentiation as ad
  9from ... import LinearParallel as lp
 10from ... import FiniteDifferences as fd
 11from ... import Selling
 12from ...FiniteDifferences import common_field
 13from .. import misc
 14from ..riemann import Riemann
 15from .implicit_base import ImplicitBase
 16from .thomsen_data import ThomsenElasticMaterial
 17
 18class Hooke(ImplicitBase):
 19	r"""
 20	The *dual* norm defined by a Hooke tensor takes the form 
 21	$$
 22	F^*(x) = \max_{|y|\leq 1} \sqrt{\sum_{ijkl} c_{ijkl} x_i y_j x_k y_l}
 23	$$
 24	where c is the Hooke tensor, and y ranges over the unit ball.
 25	The primal norm is obtained implicitly, by solving an optimization problem.
 26
 27	These norms characterize the arrival time of pressure waves in elasticity. 
 28	They are often encountered in seismic traveltime tomography.
 29
 30	Member fields and __init__ arguments : 
 31	- hooke : an array of shape (hdim,hdim,n1,...,nk) where hdim = vdim*(vdim+1)/2
 32	and vdim is the ambient space dimension. The array must be symmetric, and encodes the
 33	hooke tensor c in Voigt notation.
 34	- *args,**kwargs (optional) : passed to ImplicitBase
 35	"""
 36	def __init__(self,hooke,*args,**kwargs):
 37		super(Hooke,self).__init__(*args,**kwargs)
 38		self.hooke = hooke
 39		self._to_common_field()
 40
 41	def is_definite(self):
 42		return Riemann(self.hooke).is_definite()
 43
 44	@staticmethod
 45	def _vdim(hdim):
 46		"""Vector dimension from Hooke tensor size"""
 47		vdim = int(np.sqrt(2*hdim))
 48		if Hooke._hdim(vdim)!=hdim:
 49			raise ValueError("Hooke tensor has inconsistent dimensions.")
 50		return vdim
 51
 52	@staticmethod
 53	def _hdim(vdim):
 54		"""Hooke tensor size from vector dimension"""
 55		return (vdim*(vdim+1))//2
 56
 57	@property
 58	def vdim(self):
 59		return self._vdim(len(self.hooke))
 60
 61	@property
 62	def shape(self): return self.hooke.shape[2:]
 63	
 64	def model_HFM(self):
 65		d = self.vdim
 66		suffix = "" if self.inverse_transformation is None else "Topographic"
 67		return f"Seismic{suffix}{d}"
 68
 69	def flatten(self):
 70		hooke = misc.flatten_symmetric_matrix(self.hooke)
 71		if self.inverse_transformation is None: 
 72			return hooke
 73		else: 
 74			inv_trans= self.inverse_transformation.reshape((self.vdim**2,)+self.shape)
 75			return np.concatenate((hooke,inv_trans),axis=0)
 76
 77	@classmethod
 78	def expand(cls,arr):
 79		return cls(misc.expand_symmetric_matrix(arr))
 80
 81	def __iter__(self):
 82		yield self.hooke
 83		for x in super(Hooke,self).__iter__():
 84			yield x
 85
 86	def with_cost(self,cost):
 87		other = copy.copy(self)
 88		hooke,cost = fd.common_field((self.hooke,cost),depths=(2,0))
 89		other.hooke = hooke / cost**2
 90		return other
 91
 92	def _to_common_field(self,*args,**kwargs):
 93		self.hooke,self.inverse_transformation = fd.common_field(
 94			(self.hooke,self.inverse_transformation),(2,2),*args,**kwargs)
 95
 96	def _dual_params(self,*args,**kwargs):
 97		return fd.common_field((self.hooke,),(2,),*args,**kwargs)
 98
 99	def _dual_level(self,v,params=None,relax=0.):
100		if params is None: params = self._dual_params(v.shape[1:])
101
102		# Contract the hooke tensor and covector
103		hooke, = params
104		Voigt,Voigti = self._Voigt,self._Voigti
105		d = self.vdim
106		m = ad.asarray([[
107			sum(v[j]*v[l] * hooke[Voigt[i,j],Voigt[k,l]] 
108				for j in range(d) for l in range(d))
109			for i in range(d)] for k in range(d)])
110
111		# Evaluate det
112		s = np.exp(-relax)
113		xp = ad.cupy_generic.get_array_module(m)
114		ident = fd.as_field(xp.eye(d,dtype=m.dtype),m.shape[2:],depth=2)
115		return 1.-s -lp.det(ident - m*s) 
116
117	def extract_xz(self):
118		"""
119		Extract a two dimensional Hooke tensor from a three dimensional one, 
120		corresponding to a slice through the X and Z axes.
121		"""
122		assert self.vdim==3
123		h=self.hooke
124		return Hooke(ad.asarray([ 
125			[h[0,0], h[0,2], h[0,4] ],
126			[h[2,0], h[2,2], h[2,4] ],
127			[h[4,0], h[4,2], h[4,4] ]
128			]))
129
130	@classmethod
131	def from_VTI_2(cls,Vp,Vs,ε,δ):
132		"""
133		X,Z slice of a Vertical Transverse Isotropic medium
134		based on Thomsen parameters
135		"""
136		c33=Vp**2
137		c44=Vs**2
138		c11=c33*(1+2*ε)
139		c13=-c44+np.sqrt( (c33-c44)**2+2*δ*c33*(c33-c44) )
140		return cls.from_hexagonal(c11,None,c13,c33,c44,vdim=2)
141
142	@classmethod
143	def from_Ellipse(cls,m):
144		"""
145		Rank deficient Hooke tensor,
146		equivalent, for pressure waves, to the Riemannian metric defined by $m ^ {-2}$.
147		Shear waves are infinitely slow.
148		"""
149		assert(len(m)==2)
150		a,b,c=m[0,0],m[1,1],m[0,1]
151		return Hooke(ad.asarray( [ [a*a, a*b,a*c], [a*b, b*b, b*c], [a*c, b*c, c*c] ] ))
152
153	@classmethod
154	def from_cast(cls,metric): 
155		if isinstance(metric,cls):	return metric
156		riemann = Riemann.from_cast(metric)
157		
158		m = riemann.dual().m
159		assert not ad.is_ad(m)
160		from scipy.linalg import sqrtm
161		return cls.from_Ellipse(sqrtm(m))
162
163	def _iter_implicit(self):
164		yield self.hooke
165
166	@property	
167	def _Voigt(self):
168		"""Direct Voigt indices"""
169		if self.vdim==2:   return np.array([[0,2],[2,1]])
170		elif self.vdim==3: return np.array([[0,5,4],[5,1,3],[4,3,2]])
171		else: raise ValueError("Unsupported dimension")
172	@property
173	def _Voigti(self):
174		"""Inverse Voigt indices"""
175		if self.vdim==2:   return np.array([[0,0],[1,1],[0,1]])
176		elif self.vdim==3: return np.array([[0,0],[1,1],[2,2],[1,2],[0,2],[0,1]])
177		else: raise ValueError("Unsupported dimension")
178
179	def to_depth4(self):
180		"""
181		Produces the full Hooke tensor, of shape
182		(vdim,vdim,vdim,vdim, n1,...,nk)
183		where vdim is the ambient space dimension.
184		"""
185		Voigt = self._Voigt
186		d = self.vdim
187		return ad.array([ [ [ [ self.hooke[Voigt[i,j],Voigt[k,l]]
188			for i in range(d)] for j in range(d)] for k in range(d)] for l in range(d)])
189
190	def rotate(self,r):
191		other = copy.copy(self)
192		hooke,r = common_field((self.hooke,r),(2,2))
193		Voigti = self._Voigti
194		R = ad.array([ [ 
195			r[i0,i1]*r[j0,j1] if i1==j1 else (r[i0,i1]*r[j0,j1]+r[j0,i1]*r[i0,j1])
196			for (i0,j0) in Voigti] for (i1,j1) in Voigti])
197		other.hooke = lp.dot_AA(lp.transpose(R),lp.dot_AA(hooke,R))
198		return other
199
200	@staticmethod
201	def _Mandel_factors(vdim,shape=tuple(),a=np.sqrt(2)):
202		def f(k):	return 1. if k<vdim else a
203		hdim = Hooke._hdim(vdim)
204		factors = ad.array([[f(i)*f(j) for i in range(hdim)] for j in range(hdim)])
205		return fd.as_field(factors,shape,conditional=False)
206
207	def to_Mandel(self,a=np.sqrt(2)):
208		r"""Introduces the $\sqrt 2$ and $2$ factors involved in Mandel's notation"""
209		return self.hooke*self._Mandel_factors(self.vdim,self.shape)
210
211	@classmethod
212	def from_Mandel(cls,mandel,a=np.sqrt(2)):
213		r"""Removes the $\sqrt 2$ and $2$ factors involved in Mandel's notation"""
214		vdim = cls._vdim(len(mandel))
215		return Hooke(mandel/cls._Mandel_factors(vdim,mandel.shape[2:],a))
216
217	@classmethod
218	def from_orthorombic(cls,c11,c12,c13,c22,c23,c33,c44,c55,c66):
219		z=0*c11 # np.zeros_like(c11) raises issue in combination with cupy
220		return cls(ad.array([
221		[c11,c12,c13,  z,  z,  z],
222		[c12,c22,c23,  z,  z,  z],
223		[c13,c23,c33,  z,  z,  z],
224		[  z,  z,  z,c44,  z,  z],
225		[  z,  z,  z,  z,c55,  z],
226		[  z,  z,  z,  z,  z,c66]
227		]))
228
229	@classmethod
230	def from_orthorombic2(cls,c11,c21,c22,c31,c32,c33,c44,c55,c66): 
231		"""Orthorombic medium with a different ordering of the first block coefficients"""
232		c12,c13,c23 = c21,c31,c32
233		return cls.from_orthorombic(c11,c12,c13,c22,c23,c33,c44,c55,c66)
234
235	@classmethod
236	def from_tetragonal(cls,c11,c12,c13,c33,c44,c66):
237		c22,c23,c55 = c11,c13,c44
238		return cls.from_orthorombic(c11,c12,c13,c22,c23,c33,c44,c55,c66)
239
240	@classmethod
241	def from_hexagonal(cls,c11,c12,c13,c33,c44,vdim=3):
242		if vdim==2:
243			z = 0*c11
244			return cls(ad.asarray( [ [c11,c13,z], [c13,c33,z], [z,z,c44] ] ))
245		c66 = (c11-c12)/2.
246		return cls.from_tetragonal(c11,c12,c13,c33,c44,c66)
247
248	@classmethod 
249	def from_ThomsenElastic(cls,tem):
250		"""Hooke tensor (m/s)^2 and density (g/cm^3)"""
251		if not isinstance(tem,ThomsenElasticMaterial): tem = ThomsenElasticMaterial(*tem)
252		hex,ρ = tem.to_hexagonal()
253		return cls.from_hexagonal(*hex),ρ
254
255	def to_orthorombic(self):
256		"""Inverse function of from_orthorombic. No reconstruction check."""
257		assert self.vdim==3
258		return tuple(self.hooke[i,j] for i,j in 
259			((0,0),(0,1),(0,2),(1,1),(1,2),(2,2),(3,3),(4,4),(5,5)))
260	def to_orthorombic2(self):
261		a,b,c,d,e,f,g,h,i = self.to_orthorombic()
262		return (a,b,d,c,e,f,g,h,i)
263	def to_tetragonal(self):
264		a,b,c,_a,_c,d,e,_e,f = self.to_orthorombic()
265		return (a,b,c,d,e,f)
266	def to_hexagonal(self):
267		a,b,c,d,e,_a_b_2 = self.to_tetragonal()
268		return (a,b,c,d,e)
269
270	def is_TTI(self,tol=None):
271		"""
272		Determine if the metric is in a TTI form.
273		"""
274		# Original code by F. Desquilbet, 2020
275		if tol is None: # small value (acts as zero for floats)
276			tol = max(1e-9, MA(hooke)*1e-12)
277
278		def small(arr): return np.max(np.abs(arr))<tol
279		is_Sym = small(hooke-lp.transpose(hooke)) # symmetrical
280
281		if metric.vdim==2:
282			return is_Sym and small(hooke[2,0]) and small(hooke[2,1])
283		if metric.vdim==3:
284			return (is_Sym 
285				and small((hooke[0,0]-hooke[0,1])/2-hooke[5,5])
286				and all(small(hooke[i,j]-hooke[k,l]) 
287					for ((i,j),(k,l)) in [((0,0),(1,1)), ((2,0),(2,1)), ((3,3),(4,4))])
288				and all(small(hooke[i,j]) 
289					for (i,j) in [(3,0),(4,0),(5,0),(3,1),(4,1),(5,1),
290					(3,2),(4,2),(5,2),(4,3),(5,3),(5,4)]) ) 
291
292	def _Voigt_m2v(self,m,sym=True):
293		"""
294		Turns a symmetric matrix into a vector, based on Voigt convention.
295		- sym : True -> use the upper triangular part of m. 
296				False -> symmetrize the matrix m (adding its transpose).
297		"""
298		assert(self.inverse_transformation is None)
299		m=ad.asarray(m)
300		vdim = self.vdim
301		assert(m.shape[:2]==(vdim,vdim))
302		if vdim==1:
303			return m[0]
304		elif vdim==2:
305			if sym: return ad.array((m[0,0],m[1,1],2*m[0,1]))
306			else:   return ad.array((m[0,0],m[1,1],m[0,1]+m[1,0]))
307		elif vdim==3:
308			if sym: return ad.array((m[0,0],m[1,1],m[2,2],
309				2*m[1,2],2*m[0,2],2*m[0,1]))
310			else:   return ad.array((m1[0,0],m1[1,1],m[2,2],
311				m[1,2]+m[2,1],m[0,2]+m[2,0],m[0,1]+m[1,0]))
312		else:
313			raise ValueError("Unsupported dimension")
314
315	def dot_A(self,m,sym=True):
316		"""
317		Dot product associated with a Hooke tensor, which turns a strain tensor epsilon
318		into a stress tensor sigma.
319
320		Input:
321		- m : the strain tensor.
322		"""
323		v,hooke = fd.common_field((self._Voigt_m2v(m,sym),self.hooke),(1,2))
324		w = lp.dot_AV(hooke,v)
325		return ad.array( ((w[0],w[2]),(w[2],w[1])) )
326
327	def dot_AA(self,m1,m2=None,sym=True):
328		"""
329		Inner product associated with a Hooke tensor, on the space of symmetric matrices.
330
331		Inputs:
332		- m1 : first symmetric matrix
333		- m2 : second symmetric matrix. Defaults to m1.
334		"""
335		if m2 is None: 
336			v1,hooke = fd.common_field((self._Voigt_m2v(m1,sym),self.hooke),(1,2))
337			v2=v1
338		else: 
339			v1,v2,hooke = fd.common_field(
340				(self._Voigt_m2v(m1,sym),self._Voigt_m2v(m2,sym),self.hooke),(1,1,2))
341		return lp.dot_VV(v1,lp.dot_AV(hooke,v2))
342
343	def Selling(self):
344		r"""
345		Returns a decomposition of the hooke tensor in the mathematical form
346		$$
347		hooke = \sum_i \rho_i m_i  m_i^\top,
348		$$
349		where $\rho_i$ is a non-negative coefficient, $m_i$ is symmetric nonzero and has 
350		integer entries, and $\sum_i \rho_i$ is maximal. 
351		"""
352		assert(self.inverse_transformation is None)
353		if self.vdim<=2: coefs,offsets = Selling.Decomposition(self.hooke)
354		else: 
355			from ... import Eikonal
356			coefs,offsets = Eikonal.VoronoiDecomposition(self.hooke)
357		if self.vdim==1: 
358			moffsets = np.expand_dims(offsets,axis=0)
359		elif self.vdim==2:
360			moffsets = ad.array(((offsets[0],offsets[2]),(offsets[2],offsets[1])))
361		elif self.vdim==3:
362			moffsets = ad.array(( #Voigt notation
363				(offsets[0],offsets[5],offsets[4]),
364				(offsets[5],offsets[1],offsets[3]),
365				(offsets[4],offsets[3],offsets[2])))
366		else :
367			raise ValueError("Unsupported dimension")
368		return coefs,moffsets
369
370	def apply_transform(self):
371		"""
372		Applies the transformation, if any stored, to the hooke tensor. 
373
374		CAUTION : this feature is required for some applications to elasticity, 
375		but is incompatible with the eikonal equation solver.
376		"""
377		r = self.inverse_transformation
378		if r is None: return self
379		other = copy.copy(self)
380		other.inverse_transformation = None
381		return other.rotate(lp.transpose(r)) 
382
383	@classmethod
384	def from_Lame(cls,λ,μ,vdim=2):
385		"""
386		Constructs a Hooke tensor from the Lame coefficients, in dimension 2 or 3.
387		Positive definite provided μ>0 and 2μ+dλ>0
388		"""
389		z,s = 0*λ,λ+2*μ
390		if vdim==2: return cls(ad.array([
391			[s,λ,z],
392			[λ,s,z],
393			[z,z,μ]]))
394		elif vdim==3: return cls(ad.array([
395			[s,λ,λ,z,z,z],
396			[λ,s,λ,z,z,z],
397			[λ,λ,s,z,z,z],
398			[z,z,z,μ,z,z],
399			[z,z,z,z,μ,z],
400			[z,z,z,z,z,μ]]))
401		else: raise ValueError("Unsupported dimension")
402
403	def contract(self,w):
404		r"""Returns the contracted tensor $\sum_{j,l}c_{ijkl} w_j w_l$."""
405		voi = self._Voigt
406		hooke,w = fd.common_field((self.hooke,w),depths=(2,1))
407		def c(i,j,k,l): return hooke[voi[i,j],voi[k,l]]
408		d = self.vdim; assert len(w)==d
409		return ad.array([[
410			sum(c(i,j,k,l)*w[j]*w[l] for j in range(d) for l in range(d))
411			for i in range(d)] for k in range(d)])
412
413	def waves(self,k,ρ):
414		"""Returns the pulsation and direction of the waves with the given wave vector."""
415		m = np.moveaxis(self.contract(k),(0,1),(-2,-1))
416		eVal,eVec = np.linalg.eigh(m)
417		eVec = np.moveaxis(eVec,(-2,-1),(0,1))
418		return np.sqrt(eVal/ρ),eVec
419
420
421
422
423# Hooke tensor (m/s)^2 and density (g/cm^3)
424# Reference : Lecomte, I. (1993). Finite difference calculation of first traveltimes 
425# in anisotropic media 1. Geophysical Journal International, 113(2), 318–342.
426Hooke.mica = Hooke.from_hexagonal(178.,42.4,14.5,54.9,12.2), 2.79
427Hooke.stishovite = Hooke.from_tetragonal(453,211,203,776,252,302), 4.29
428Hooke.olivine = Hooke.from_orthorombic(323.7,66.4,71.6,197.6,75.6,235.1,64.6,78.7,79.0), 3.311
 19class Hooke(ImplicitBase):
 20	r"""
 21	The *dual* norm defined by a Hooke tensor takes the form 
 22	$$
 23	F^*(x) = \max_{|y|\leq 1} \sqrt{\sum_{ijkl} c_{ijkl} x_i y_j x_k y_l}
 24	$$
 25	where c is the Hooke tensor, and y ranges over the unit ball.
 26	The primal norm is obtained implicitly, by solving an optimization problem.
 27
 28	These norms characterize the arrival time of pressure waves in elasticity. 
 29	They are often encountered in seismic traveltime tomography.
 30
 31	Member fields and __init__ arguments : 
 32	- hooke : an array of shape (hdim,hdim,n1,...,nk) where hdim = vdim*(vdim+1)/2
 33	and vdim is the ambient space dimension. The array must be symmetric, and encodes the
 34	hooke tensor c in Voigt notation.
 35	- *args,**kwargs (optional) : passed to ImplicitBase
 36	"""
 37	def __init__(self,hooke,*args,**kwargs):
 38		super(Hooke,self).__init__(*args,**kwargs)
 39		self.hooke = hooke
 40		self._to_common_field()
 41
 42	def is_definite(self):
 43		return Riemann(self.hooke).is_definite()
 44
 45	@staticmethod
 46	def _vdim(hdim):
 47		"""Vector dimension from Hooke tensor size"""
 48		vdim = int(np.sqrt(2*hdim))
 49		if Hooke._hdim(vdim)!=hdim:
 50			raise ValueError("Hooke tensor has inconsistent dimensions.")
 51		return vdim
 52
 53	@staticmethod
 54	def _hdim(vdim):
 55		"""Hooke tensor size from vector dimension"""
 56		return (vdim*(vdim+1))//2
 57
 58	@property
 59	def vdim(self):
 60		return self._vdim(len(self.hooke))
 61
 62	@property
 63	def shape(self): return self.hooke.shape[2:]
 64	
 65	def model_HFM(self):
 66		d = self.vdim
 67		suffix = "" if self.inverse_transformation is None else "Topographic"
 68		return f"Seismic{suffix}{d}"
 69
 70	def flatten(self):
 71		hooke = misc.flatten_symmetric_matrix(self.hooke)
 72		if self.inverse_transformation is None: 
 73			return hooke
 74		else: 
 75			inv_trans= self.inverse_transformation.reshape((self.vdim**2,)+self.shape)
 76			return np.concatenate((hooke,inv_trans),axis=0)
 77
 78	@classmethod
 79	def expand(cls,arr):
 80		return cls(misc.expand_symmetric_matrix(arr))
 81
 82	def __iter__(self):
 83		yield self.hooke
 84		for x in super(Hooke,self).__iter__():
 85			yield x
 86
 87	def with_cost(self,cost):
 88		other = copy.copy(self)
 89		hooke,cost = fd.common_field((self.hooke,cost),depths=(2,0))
 90		other.hooke = hooke / cost**2
 91		return other
 92
 93	def _to_common_field(self,*args,**kwargs):
 94		self.hooke,self.inverse_transformation = fd.common_field(
 95			(self.hooke,self.inverse_transformation),(2,2),*args,**kwargs)
 96
 97	def _dual_params(self,*args,**kwargs):
 98		return fd.common_field((self.hooke,),(2,),*args,**kwargs)
 99
100	def _dual_level(self,v,params=None,relax=0.):
101		if params is None: params = self._dual_params(v.shape[1:])
102
103		# Contract the hooke tensor and covector
104		hooke, = params
105		Voigt,Voigti = self._Voigt,self._Voigti
106		d = self.vdim
107		m = ad.asarray([[
108			sum(v[j]*v[l] * hooke[Voigt[i,j],Voigt[k,l]] 
109				for j in range(d) for l in range(d))
110			for i in range(d)] for k in range(d)])
111
112		# Evaluate det
113		s = np.exp(-relax)
114		xp = ad.cupy_generic.get_array_module(m)
115		ident = fd.as_field(xp.eye(d,dtype=m.dtype),m.shape[2:],depth=2)
116		return 1.-s -lp.det(ident - m*s) 
117
118	def extract_xz(self):
119		"""
120		Extract a two dimensional Hooke tensor from a three dimensional one, 
121		corresponding to a slice through the X and Z axes.
122		"""
123		assert self.vdim==3
124		h=self.hooke
125		return Hooke(ad.asarray([ 
126			[h[0,0], h[0,2], h[0,4] ],
127			[h[2,0], h[2,2], h[2,4] ],
128			[h[4,0], h[4,2], h[4,4] ]
129			]))
130
131	@classmethod
132	def from_VTI_2(cls,Vp,Vs,ε,δ):
133		"""
134		X,Z slice of a Vertical Transverse Isotropic medium
135		based on Thomsen parameters
136		"""
137		c33=Vp**2
138		c44=Vs**2
139		c11=c33*(1+2*ε)
140		c13=-c44+np.sqrt( (c33-c44)**2+2*δ*c33*(c33-c44) )
141		return cls.from_hexagonal(c11,None,c13,c33,c44,vdim=2)
142
143	@classmethod
144	def from_Ellipse(cls,m):
145		"""
146		Rank deficient Hooke tensor,
147		equivalent, for pressure waves, to the Riemannian metric defined by $m ^ {-2}$.
148		Shear waves are infinitely slow.
149		"""
150		assert(len(m)==2)
151		a,b,c=m[0,0],m[1,1],m[0,1]
152		return Hooke(ad.asarray( [ [a*a, a*b,a*c], [a*b, b*b, b*c], [a*c, b*c, c*c] ] ))
153
154	@classmethod
155	def from_cast(cls,metric): 
156		if isinstance(metric,cls):	return metric
157		riemann = Riemann.from_cast(metric)
158		
159		m = riemann.dual().m
160		assert not ad.is_ad(m)
161		from scipy.linalg import sqrtm
162		return cls.from_Ellipse(sqrtm(m))
163
164	def _iter_implicit(self):
165		yield self.hooke
166
167	@property	
168	def _Voigt(self):
169		"""Direct Voigt indices"""
170		if self.vdim==2:   return np.array([[0,2],[2,1]])
171		elif self.vdim==3: return np.array([[0,5,4],[5,1,3],[4,3,2]])
172		else: raise ValueError("Unsupported dimension")
173	@property
174	def _Voigti(self):
175		"""Inverse Voigt indices"""
176		if self.vdim==2:   return np.array([[0,0],[1,1],[0,1]])
177		elif self.vdim==3: return np.array([[0,0],[1,1],[2,2],[1,2],[0,2],[0,1]])
178		else: raise ValueError("Unsupported dimension")
179
180	def to_depth4(self):
181		"""
182		Produces the full Hooke tensor, of shape
183		(vdim,vdim,vdim,vdim, n1,...,nk)
184		where vdim is the ambient space dimension.
185		"""
186		Voigt = self._Voigt
187		d = self.vdim
188		return ad.array([ [ [ [ self.hooke[Voigt[i,j],Voigt[k,l]]
189			for i in range(d)] for j in range(d)] for k in range(d)] for l in range(d)])
190
191	def rotate(self,r):
192		other = copy.copy(self)
193		hooke,r = common_field((self.hooke,r),(2,2))
194		Voigti = self._Voigti
195		R = ad.array([ [ 
196			r[i0,i1]*r[j0,j1] if i1==j1 else (r[i0,i1]*r[j0,j1]+r[j0,i1]*r[i0,j1])
197			for (i0,j0) in Voigti] for (i1,j1) in Voigti])
198		other.hooke = lp.dot_AA(lp.transpose(R),lp.dot_AA(hooke,R))
199		return other
200
201	@staticmethod
202	def _Mandel_factors(vdim,shape=tuple(),a=np.sqrt(2)):
203		def f(k):	return 1. if k<vdim else a
204		hdim = Hooke._hdim(vdim)
205		factors = ad.array([[f(i)*f(j) for i in range(hdim)] for j in range(hdim)])
206		return fd.as_field(factors,shape,conditional=False)
207
208	def to_Mandel(self,a=np.sqrt(2)):
209		r"""Introduces the $\sqrt 2$ and $2$ factors involved in Mandel's notation"""
210		return self.hooke*self._Mandel_factors(self.vdim,self.shape)
211
212	@classmethod
213	def from_Mandel(cls,mandel,a=np.sqrt(2)):
214		r"""Removes the $\sqrt 2$ and $2$ factors involved in Mandel's notation"""
215		vdim = cls._vdim(len(mandel))
216		return Hooke(mandel/cls._Mandel_factors(vdim,mandel.shape[2:],a))
217
218	@classmethod
219	def from_orthorombic(cls,c11,c12,c13,c22,c23,c33,c44,c55,c66):
220		z=0*c11 # np.zeros_like(c11) raises issue in combination with cupy
221		return cls(ad.array([
222		[c11,c12,c13,  z,  z,  z],
223		[c12,c22,c23,  z,  z,  z],
224		[c13,c23,c33,  z,  z,  z],
225		[  z,  z,  z,c44,  z,  z],
226		[  z,  z,  z,  z,c55,  z],
227		[  z,  z,  z,  z,  z,c66]
228		]))
229
230	@classmethod
231	def from_orthorombic2(cls,c11,c21,c22,c31,c32,c33,c44,c55,c66): 
232		"""Orthorombic medium with a different ordering of the first block coefficients"""
233		c12,c13,c23 = c21,c31,c32
234		return cls.from_orthorombic(c11,c12,c13,c22,c23,c33,c44,c55,c66)
235
236	@classmethod
237	def from_tetragonal(cls,c11,c12,c13,c33,c44,c66):
238		c22,c23,c55 = c11,c13,c44
239		return cls.from_orthorombic(c11,c12,c13,c22,c23,c33,c44,c55,c66)
240
241	@classmethod
242	def from_hexagonal(cls,c11,c12,c13,c33,c44,vdim=3):
243		if vdim==2:
244			z = 0*c11
245			return cls(ad.asarray( [ [c11,c13,z], [c13,c33,z], [z,z,c44] ] ))
246		c66 = (c11-c12)/2.
247		return cls.from_tetragonal(c11,c12,c13,c33,c44,c66)
248
249	@classmethod 
250	def from_ThomsenElastic(cls,tem):
251		"""Hooke tensor (m/s)^2 and density (g/cm^3)"""
252		if not isinstance(tem,ThomsenElasticMaterial): tem = ThomsenElasticMaterial(*tem)
253		hex,ρ = tem.to_hexagonal()
254		return cls.from_hexagonal(*hex),ρ
255
256	def to_orthorombic(self):
257		"""Inverse function of from_orthorombic. No reconstruction check."""
258		assert self.vdim==3
259		return tuple(self.hooke[i,j] for i,j in 
260			((0,0),(0,1),(0,2),(1,1),(1,2),(2,2),(3,3),(4,4),(5,5)))
261	def to_orthorombic2(self):
262		a,b,c,d,e,f,g,h,i = self.to_orthorombic()
263		return (a,b,d,c,e,f,g,h,i)
264	def to_tetragonal(self):
265		a,b,c,_a,_c,d,e,_e,f = self.to_orthorombic()
266		return (a,b,c,d,e,f)
267	def to_hexagonal(self):
268		a,b,c,d,e,_a_b_2 = self.to_tetragonal()
269		return (a,b,c,d,e)
270
271	def is_TTI(self,tol=None):
272		"""
273		Determine if the metric is in a TTI form.
274		"""
275		# Original code by F. Desquilbet, 2020
276		if tol is None: # small value (acts as zero for floats)
277			tol = max(1e-9, MA(hooke)*1e-12)
278
279		def small(arr): return np.max(np.abs(arr))<tol
280		is_Sym = small(hooke-lp.transpose(hooke)) # symmetrical
281
282		if metric.vdim==2:
283			return is_Sym and small(hooke[2,0]) and small(hooke[2,1])
284		if metric.vdim==3:
285			return (is_Sym 
286				and small((hooke[0,0]-hooke[0,1])/2-hooke[5,5])
287				and all(small(hooke[i,j]-hooke[k,l]) 
288					for ((i,j),(k,l)) in [((0,0),(1,1)), ((2,0),(2,1)), ((3,3),(4,4))])
289				and all(small(hooke[i,j]) 
290					for (i,j) in [(3,0),(4,0),(5,0),(3,1),(4,1),(5,1),
291					(3,2),(4,2),(5,2),(4,3),(5,3),(5,4)]) ) 
292
293	def _Voigt_m2v(self,m,sym=True):
294		"""
295		Turns a symmetric matrix into a vector, based on Voigt convention.
296		- sym : True -> use the upper triangular part of m. 
297				False -> symmetrize the matrix m (adding its transpose).
298		"""
299		assert(self.inverse_transformation is None)
300		m=ad.asarray(m)
301		vdim = self.vdim
302		assert(m.shape[:2]==(vdim,vdim))
303		if vdim==1:
304			return m[0]
305		elif vdim==2:
306			if sym: return ad.array((m[0,0],m[1,1],2*m[0,1]))
307			else:   return ad.array((m[0,0],m[1,1],m[0,1]+m[1,0]))
308		elif vdim==3:
309			if sym: return ad.array((m[0,0],m[1,1],m[2,2],
310				2*m[1,2],2*m[0,2],2*m[0,1]))
311			else:   return ad.array((m1[0,0],m1[1,1],m[2,2],
312				m[1,2]+m[2,1],m[0,2]+m[2,0],m[0,1]+m[1,0]))
313		else:
314			raise ValueError("Unsupported dimension")
315
316	def dot_A(self,m,sym=True):
317		"""
318		Dot product associated with a Hooke tensor, which turns a strain tensor epsilon
319		into a stress tensor sigma.
320
321		Input:
322		- m : the strain tensor.
323		"""
324		v,hooke = fd.common_field((self._Voigt_m2v(m,sym),self.hooke),(1,2))
325		w = lp.dot_AV(hooke,v)
326		return ad.array( ((w[0],w[2]),(w[2],w[1])) )
327
328	def dot_AA(self,m1,m2=None,sym=True):
329		"""
330		Inner product associated with a Hooke tensor, on the space of symmetric matrices.
331
332		Inputs:
333		- m1 : first symmetric matrix
334		- m2 : second symmetric matrix. Defaults to m1.
335		"""
336		if m2 is None: 
337			v1,hooke = fd.common_field((self._Voigt_m2v(m1,sym),self.hooke),(1,2))
338			v2=v1
339		else: 
340			v1,v2,hooke = fd.common_field(
341				(self._Voigt_m2v(m1,sym),self._Voigt_m2v(m2,sym),self.hooke),(1,1,2))
342		return lp.dot_VV(v1,lp.dot_AV(hooke,v2))
343
344	def Selling(self):
345		r"""
346		Returns a decomposition of the hooke tensor in the mathematical form
347		$$
348		hooke = \sum_i \rho_i m_i  m_i^\top,
349		$$
350		where $\rho_i$ is a non-negative coefficient, $m_i$ is symmetric nonzero and has 
351		integer entries, and $\sum_i \rho_i$ is maximal. 
352		"""
353		assert(self.inverse_transformation is None)
354		if self.vdim<=2: coefs,offsets = Selling.Decomposition(self.hooke)
355		else: 
356			from ... import Eikonal
357			coefs,offsets = Eikonal.VoronoiDecomposition(self.hooke)
358		if self.vdim==1: 
359			moffsets = np.expand_dims(offsets,axis=0)
360		elif self.vdim==2:
361			moffsets = ad.array(((offsets[0],offsets[2]),(offsets[2],offsets[1])))
362		elif self.vdim==3:
363			moffsets = ad.array(( #Voigt notation
364				(offsets[0],offsets[5],offsets[4]),
365				(offsets[5],offsets[1],offsets[3]),
366				(offsets[4],offsets[3],offsets[2])))
367		else :
368			raise ValueError("Unsupported dimension")
369		return coefs,moffsets
370
371	def apply_transform(self):
372		"""
373		Applies the transformation, if any stored, to the hooke tensor. 
374
375		CAUTION : this feature is required for some applications to elasticity, 
376		but is incompatible with the eikonal equation solver.
377		"""
378		r = self.inverse_transformation
379		if r is None: return self
380		other = copy.copy(self)
381		other.inverse_transformation = None
382		return other.rotate(lp.transpose(r)) 
383
384	@classmethod
385	def from_Lame(cls,λ,μ,vdim=2):
386		"""
387		Constructs a Hooke tensor from the Lame coefficients, in dimension 2 or 3.
388		Positive definite provided μ>0 and 2μ+dλ>0
389		"""
390		z,s = 0*λ,λ+2*μ
391		if vdim==2: return cls(ad.array([
392			[s,λ,z],
393			[λ,s,z],
394			[z,z,μ]]))
395		elif vdim==3: return cls(ad.array([
396			[s,λ,λ,z,z,z],
397			[λ,s,λ,z,z,z],
398			[λ,λ,s,z,z,z],
399			[z,z,z,μ,z,z],
400			[z,z,z,z,μ,z],
401			[z,z,z,z,z,μ]]))
402		else: raise ValueError("Unsupported dimension")
403
404	def contract(self,w):
405		r"""Returns the contracted tensor $\sum_{j,l}c_{ijkl} w_j w_l$."""
406		voi = self._Voigt
407		hooke,w = fd.common_field((self.hooke,w),depths=(2,1))
408		def c(i,j,k,l): return hooke[voi[i,j],voi[k,l]]
409		d = self.vdim; assert len(w)==d
410		return ad.array([[
411			sum(c(i,j,k,l)*w[j]*w[l] for j in range(d) for l in range(d))
412			for i in range(d)] for k in range(d)])
413
414	def waves(self,k,ρ):
415		"""Returns the pulsation and direction of the waves with the given wave vector."""
416		m = np.moveaxis(self.contract(k),(0,1),(-2,-1))
417		eVal,eVec = np.linalg.eigh(m)
418		eVec = np.moveaxis(eVec,(-2,-1),(0,1))
419		return np.sqrt(eVal/ρ),eVec

The dual norm defined by a Hooke tensor takes the form $$ F^*(x) = \max_{|y|\leq 1} \sqrt{\sum_{ijkl} c_{ijkl} x_i y_j x_k y_l} $$ where c is the Hooke tensor, and y ranges over the unit ball. The primal norm is obtained implicitly, by solving an optimization problem.

These norms characterize the arrival time of pressure waves in elasticity. They are often encountered in seismic traveltime tomography.

Member fields and __init__ arguments :

  • hooke : an array of shape (hdim,hdim,n1,...,nk) where hdim = vdim*(vdim+1)/2 and vdim is the ambient space dimension. The array must be symmetric, and encodes the hooke tensor c in Voigt notation.
  • args,*kwargs (optional) : passed to ImplicitBase
Hooke(hooke, *args, **kwargs)
37	def __init__(self,hooke,*args,**kwargs):
38		super(Hooke,self).__init__(*args,**kwargs)
39		self.hooke = hooke
40		self._to_common_field()
hooke
def is_definite(self):
42	def is_definite(self):
43		return Riemann(self.hooke).is_definite()

Attempts to check wether the data defines a mathematically valid norm.

vdim
58	@property
59	def vdim(self):
60		return self._vdim(len(self.hooke))

The ambient vector space dimension, often denoted $d$ in mathematical formulas.

shape
62	@property
63	def shape(self): return self.hooke.shape[2:]

Dimensions of the underlying domain. Expected to be the empty tuple, or a tuple of length vdim.

def model_HFM(self):
65	def model_HFM(self):
66		d = self.vdim
67		suffix = "" if self.inverse_transformation is None else "Topographic"
68		return f"Seismic{suffix}{d}"

The name of the 'model' for parameter, as input to the HFM library.

def flatten(self):
70	def flatten(self):
71		hooke = misc.flatten_symmetric_matrix(self.hooke)
72		if self.inverse_transformation is None: 
73			return hooke
74		else: 
75			inv_trans= self.inverse_transformation.reshape((self.vdim**2,)+self.shape)
76			return np.concatenate((hooke,inv_trans),axis=0)

Flattens and concatenate the member fields into a single array.

@classmethod
def expand(cls, arr):
78	@classmethod
79	def expand(cls,arr):
80		return cls(misc.expand_symmetric_matrix(arr))

Inverse of the flatten member function. Turns a suitable array into a metric.

def with_cost(self, cost):
87	def with_cost(self,cost):
88		other = copy.copy(self)
89		hooke,cost = fd.common_field((self.hooke,cost),depths=(2,0))
90		other.hooke = hooke / cost**2
91		return other

Produces a norm $N'$ obeying $N'(x) = N(cost*x)$.

def extract_xz(self):
118	def extract_xz(self):
119		"""
120		Extract a two dimensional Hooke tensor from a three dimensional one, 
121		corresponding to a slice through the X and Z axes.
122		"""
123		assert self.vdim==3
124		h=self.hooke
125		return Hooke(ad.asarray([ 
126			[h[0,0], h[0,2], h[0,4] ],
127			[h[2,0], h[2,2], h[2,4] ],
128			[h[4,0], h[4,2], h[4,4] ]
129			]))

Extract a two dimensional Hooke tensor from a three dimensional one, corresponding to a slice through the X and Z axes.

@classmethod
def from_VTI_2(cls, Vp, Vs, ε, δ):
131	@classmethod
132	def from_VTI_2(cls,Vp,Vs,ε,δ):
133		"""
134		X,Z slice of a Vertical Transverse Isotropic medium
135		based on Thomsen parameters
136		"""
137		c33=Vp**2
138		c44=Vs**2
139		c11=c33*(1+2*ε)
140		c13=-c44+np.sqrt( (c33-c44)**2+2*δ*c33*(c33-c44) )
141		return cls.from_hexagonal(c11,None,c13,c33,c44,vdim=2)

X,Z slice of a Vertical Transverse Isotropic medium based on Thomsen parameters

@classmethod
def from_Ellipse(cls, m):
143	@classmethod
144	def from_Ellipse(cls,m):
145		"""
146		Rank deficient Hooke tensor,
147		equivalent, for pressure waves, to the Riemannian metric defined by $m ^ {-2}$.
148		Shear waves are infinitely slow.
149		"""
150		assert(len(m)==2)
151		a,b,c=m[0,0],m[1,1],m[0,1]
152		return Hooke(ad.asarray( [ [a*a, a*b,a*c], [a*b, b*b, b*c], [a*c, b*c, c*c] ] ))

Rank deficient Hooke tensor, equivalent, for pressure waves, to the Riemannian metric defined by $m ^ {-2}$. Shear waves are infinitely slow.

@classmethod
def from_cast(cls, metric):
154	@classmethod
155	def from_cast(cls,metric): 
156		if isinstance(metric,cls):	return metric
157		riemann = Riemann.from_cast(metric)
158		
159		m = riemann.dual().m
160		assert not ad.is_ad(m)
161		from scipy.linalg import sqrtm
162		return cls.from_Ellipse(sqrtm(m))

Produces a metric by casting another metric of a compatible type.

def to_depth4(self):
180	def to_depth4(self):
181		"""
182		Produces the full Hooke tensor, of shape
183		(vdim,vdim,vdim,vdim, n1,...,nk)
184		where vdim is the ambient space dimension.
185		"""
186		Voigt = self._Voigt
187		d = self.vdim
188		return ad.array([ [ [ [ self.hooke[Voigt[i,j],Voigt[k,l]]
189			for i in range(d)] for j in range(d)] for k in range(d)] for l in range(d)])

Produces the full Hooke tensor, of shape (vdim,vdim,vdim,vdim, n1,...,nk) where vdim is the ambient space dimension.

def rotate(self, r):
191	def rotate(self,r):
192		other = copy.copy(self)
193		hooke,r = common_field((self.hooke,r),(2,2))
194		Voigti = self._Voigti
195		R = ad.array([ [ 
196			r[i0,i1]*r[j0,j1] if i1==j1 else (r[i0,i1]*r[j0,j1]+r[j0,i1]*r[i0,j1])
197			for (i0,j0) in Voigti] for (i1,j1) in Voigti])
198		other.hooke = lp.dot_AA(lp.transpose(R),lp.dot_AA(hooke,R))
199		return other

Rotation of the norm, by a given rotation matrix. The new unit ball is the direct image of the previous one.

def to_Mandel(self, a=1.4142135623730951):
208	def to_Mandel(self,a=np.sqrt(2)):
209		r"""Introduces the $\sqrt 2$ and $2$ factors involved in Mandel's notation"""
210		return self.hooke*self._Mandel_factors(self.vdim,self.shape)

Introduces the $\sqrt 2$ and $2$ factors involved in Mandel's notation

@classmethod
def from_Mandel(cls, mandel, a=1.4142135623730951):
212	@classmethod
213	def from_Mandel(cls,mandel,a=np.sqrt(2)):
214		r"""Removes the $\sqrt 2$ and $2$ factors involved in Mandel's notation"""
215		vdim = cls._vdim(len(mandel))
216		return Hooke(mandel/cls._Mandel_factors(vdim,mandel.shape[2:],a))

Removes the $\sqrt 2$ and $2$ factors involved in Mandel's notation

@classmethod
def from_orthorombic(cls, c11, c12, c13, c22, c23, c33, c44, c55, c66):
218	@classmethod
219	def from_orthorombic(cls,c11,c12,c13,c22,c23,c33,c44,c55,c66):
220		z=0*c11 # np.zeros_like(c11) raises issue in combination with cupy
221		return cls(ad.array([
222		[c11,c12,c13,  z,  z,  z],
223		[c12,c22,c23,  z,  z,  z],
224		[c13,c23,c33,  z,  z,  z],
225		[  z,  z,  z,c44,  z,  z],
226		[  z,  z,  z,  z,c55,  z],
227		[  z,  z,  z,  z,  z,c66]
228		]))
@classmethod
def from_orthorombic2(cls, c11, c21, c22, c31, c32, c33, c44, c55, c66):
230	@classmethod
231	def from_orthorombic2(cls,c11,c21,c22,c31,c32,c33,c44,c55,c66): 
232		"""Orthorombic medium with a different ordering of the first block coefficients"""
233		c12,c13,c23 = c21,c31,c32
234		return cls.from_orthorombic(c11,c12,c13,c22,c23,c33,c44,c55,c66)

Orthorombic medium with a different ordering of the first block coefficients

@classmethod
def from_tetragonal(cls, c11, c12, c13, c33, c44, c66):
236	@classmethod
237	def from_tetragonal(cls,c11,c12,c13,c33,c44,c66):
238		c22,c23,c55 = c11,c13,c44
239		return cls.from_orthorombic(c11,c12,c13,c22,c23,c33,c44,c55,c66)
@classmethod
def from_hexagonal(cls, c11, c12, c13, c33, c44, vdim=3):
241	@classmethod
242	def from_hexagonal(cls,c11,c12,c13,c33,c44,vdim=3):
243		if vdim==2:
244			z = 0*c11
245			return cls(ad.asarray( [ [c11,c13,z], [c13,c33,z], [z,z,c44] ] ))
246		c66 = (c11-c12)/2.
247		return cls.from_tetragonal(c11,c12,c13,c33,c44,c66)
@classmethod
def from_ThomsenElastic(cls, tem):
249	@classmethod 
250	def from_ThomsenElastic(cls,tem):
251		"""Hooke tensor (m/s)^2 and density (g/cm^3)"""
252		if not isinstance(tem,ThomsenElasticMaterial): tem = ThomsenElasticMaterial(*tem)
253		hex,ρ = tem.to_hexagonal()
254		return cls.from_hexagonal(*hex),ρ

Hooke tensor (m/s)^2 and density (g/cm^3)

def to_orthorombic(self):
256	def to_orthorombic(self):
257		"""Inverse function of from_orthorombic. No reconstruction check."""
258		assert self.vdim==3
259		return tuple(self.hooke[i,j] for i,j in 
260			((0,0),(0,1),(0,2),(1,1),(1,2),(2,2),(3,3),(4,4),(5,5)))

Inverse function of from_orthorombic. No reconstruction check.

def to_orthorombic2(self):
261	def to_orthorombic2(self):
262		a,b,c,d,e,f,g,h,i = self.to_orthorombic()
263		return (a,b,d,c,e,f,g,h,i)
def to_tetragonal(self):
264	def to_tetragonal(self):
265		a,b,c,_a,_c,d,e,_e,f = self.to_orthorombic()
266		return (a,b,c,d,e,f)
def to_hexagonal(self):
267	def to_hexagonal(self):
268		a,b,c,d,e,_a_b_2 = self.to_tetragonal()
269		return (a,b,c,d,e)
def is_TTI(self, tol=None):
271	def is_TTI(self,tol=None):
272		"""
273		Determine if the metric is in a TTI form.
274		"""
275		# Original code by F. Desquilbet, 2020
276		if tol is None: # small value (acts as zero for floats)
277			tol = max(1e-9, MA(hooke)*1e-12)
278
279		def small(arr): return np.max(np.abs(arr))<tol
280		is_Sym = small(hooke-lp.transpose(hooke)) # symmetrical
281
282		if metric.vdim==2:
283			return is_Sym and small(hooke[2,0]) and small(hooke[2,1])
284		if metric.vdim==3:
285			return (is_Sym 
286				and small((hooke[0,0]-hooke[0,1])/2-hooke[5,5])
287				and all(small(hooke[i,j]-hooke[k,l]) 
288					for ((i,j),(k,l)) in [((0,0),(1,1)), ((2,0),(2,1)), ((3,3),(4,4))])
289				and all(small(hooke[i,j]) 
290					for (i,j) in [(3,0),(4,0),(5,0),(3,1),(4,1),(5,1),
291					(3,2),(4,2),(5,2),(4,3),(5,3),(5,4)]) ) 

Determine if the metric is in a TTI form.

def dot_A(self, m, sym=True):
316	def dot_A(self,m,sym=True):
317		"""
318		Dot product associated with a Hooke tensor, which turns a strain tensor epsilon
319		into a stress tensor sigma.
320
321		Input:
322		- m : the strain tensor.
323		"""
324		v,hooke = fd.common_field((self._Voigt_m2v(m,sym),self.hooke),(1,2))
325		w = lp.dot_AV(hooke,v)
326		return ad.array( ((w[0],w[2]),(w[2],w[1])) )

Dot product associated with a Hooke tensor, which turns a strain tensor epsilon into a stress tensor sigma.

Input:

  • m : the strain tensor.
def dot_AA(self, m1, m2=None, sym=True):
328	def dot_AA(self,m1,m2=None,sym=True):
329		"""
330		Inner product associated with a Hooke tensor, on the space of symmetric matrices.
331
332		Inputs:
333		- m1 : first symmetric matrix
334		- m2 : second symmetric matrix. Defaults to m1.
335		"""
336		if m2 is None: 
337			v1,hooke = fd.common_field((self._Voigt_m2v(m1,sym),self.hooke),(1,2))
338			v2=v1
339		else: 
340			v1,v2,hooke = fd.common_field(
341				(self._Voigt_m2v(m1,sym),self._Voigt_m2v(m2,sym),self.hooke),(1,1,2))
342		return lp.dot_VV(v1,lp.dot_AV(hooke,v2))

Inner product associated with a Hooke tensor, on the space of symmetric matrices.

Inputs:

  • m1 : first symmetric matrix
  • m2 : second symmetric matrix. Defaults to m1.
def Selling(self):
344	def Selling(self):
345		r"""
346		Returns a decomposition of the hooke tensor in the mathematical form
347		$$
348		hooke = \sum_i \rho_i m_i  m_i^\top,
349		$$
350		where $\rho_i$ is a non-negative coefficient, $m_i$ is symmetric nonzero and has 
351		integer entries, and $\sum_i \rho_i$ is maximal. 
352		"""
353		assert(self.inverse_transformation is None)
354		if self.vdim<=2: coefs,offsets = Selling.Decomposition(self.hooke)
355		else: 
356			from ... import Eikonal
357			coefs,offsets = Eikonal.VoronoiDecomposition(self.hooke)
358		if self.vdim==1: 
359			moffsets = np.expand_dims(offsets,axis=0)
360		elif self.vdim==2:
361			moffsets = ad.array(((offsets[0],offsets[2]),(offsets[2],offsets[1])))
362		elif self.vdim==3:
363			moffsets = ad.array(( #Voigt notation
364				(offsets[0],offsets[5],offsets[4]),
365				(offsets[5],offsets[1],offsets[3]),
366				(offsets[4],offsets[3],offsets[2])))
367		else :
368			raise ValueError("Unsupported dimension")
369		return coefs,moffsets

Returns a decomposition of the hooke tensor in the mathematical form $$ hooke = \sum_i \rho_i m_i m_i^\top, $$ where $\rho_i$ is a non-negative coefficient, $m_i$ is symmetric nonzero and has integer entries, and $\sum_i \rho_i$ is maximal.

def apply_transform(self):
371	def apply_transform(self):
372		"""
373		Applies the transformation, if any stored, to the hooke tensor. 
374
375		CAUTION : this feature is required for some applications to elasticity, 
376		but is incompatible with the eikonal equation solver.
377		"""
378		r = self.inverse_transformation
379		if r is None: return self
380		other = copy.copy(self)
381		other.inverse_transformation = None
382		return other.rotate(lp.transpose(r)) 

Applies the transformation, if any stored, to the hooke tensor.

CAUTION : this feature is required for some applications to elasticity, but is incompatible with the eikonal equation solver.

@classmethod
def from_Lame(cls, λ, μ, vdim=2):
384	@classmethod
385	def from_Lame(cls,λ,μ,vdim=2):
386		"""
387		Constructs a Hooke tensor from the Lame coefficients, in dimension 2 or 3.
388		Positive definite provided μ>0 and 2μ+dλ>0
389		"""
390		z,s = 0*λ,λ+2*μ
391		if vdim==2: return cls(ad.array([
392			[s,λ,z],
393			[λ,s,z],
394			[z,z,μ]]))
395		elif vdim==3: return cls(ad.array([
396			[s,λ,λ,z,z,z],
397			[λ,s,λ,z,z,z],
398			[λ,λ,s,z,z,z],
399			[z,z,z,μ,z,z],
400			[z,z,z,z,μ,z],
401			[z,z,z,z,z,μ]]))
402		else: raise ValueError("Unsupported dimension")

Constructs a Hooke tensor from the Lame coefficients, in dimension 2 or 3. Positive definite provided μ>0 and 2μ+dλ>0

def contract(self, w):
404	def contract(self,w):
405		r"""Returns the contracted tensor $\sum_{j,l}c_{ijkl} w_j w_l$."""
406		voi = self._Voigt
407		hooke,w = fd.common_field((self.hooke,w),depths=(2,1))
408		def c(i,j,k,l): return hooke[voi[i,j],voi[k,l]]
409		d = self.vdim; assert len(w)==d
410		return ad.array([[
411			sum(c(i,j,k,l)*w[j]*w[l] for j in range(d) for l in range(d))
412			for i in range(d)] for k in range(d)])

Returns the contracted tensor $\sum_{j,l}c_{ijkl} w_j w_l$.

def waves(self, k, ρ):
414	def waves(self,k,ρ):
415		"""Returns the pulsation and direction of the waves with the given wave vector."""
416		m = np.moveaxis(self.contract(k),(0,1),(-2,-1))
417		eVal,eVec = np.linalg.eigh(m)
418		eVec = np.moveaxis(eVec,(-2,-1),(0,1))
419		return np.sqrt(eVal/ρ),eVec

Returns the pulsation and direction of the waves with the given wave vector.

mica = (Hooke(array([[178. , 42.4, 14.5, 0. , 0. , 0. ], [ 42.4, 178. , 14.5, 0. , 0. , 0. ], [ 14.5, 14.5, 54.9, 0. , 0. , 0. ], [ 0. , 0. , 0. , 12.2, 0. , 0. ], [ 0. , 0. , 0. , 0. , 12.2, 0. ], [ 0. , 0. , 0. , 0. , 0. , 67.8]]), None, 6, (), 0.0), 2.79)
stishovite = (Hooke(array([[453, 211, 203, 0, 0, 0], [211, 453, 203, 0, 0, 0], [203, 203, 776, 0, 0, 0], [ 0, 0, 0, 252, 0, 0], [ 0, 0, 0, 0, 252, 0], [ 0, 0, 0, 0, 0, 302]]), None, 6, (), 0.0), 4.29)
olivine = (Hooke(array([[323.7, 66.4, 71.6, 0. , 0. , 0. ], [ 66.4, 197.6, 75.6, 0. , 0. , 0. ], [ 71.6, 75.6, 235.1, 0. , 0. , 0. ], [ 0. , 0. , 0. , 64.6, 0. , 0. ], [ 0. , 0. , 0. , 0. , 78.7, 0. ], [ 0. , 0. , 0. , 0. , 0. , 79. ]]), None, 6, (), 0.0), 3.311)