@@ -188,6 +188,200 @@ def to_mij(rho, v, w, kappa, sigma, h):
188188 return np .array ([mt0 , mt1 , mt2 , mt3 , mt4 , mt5 ])
189189
190190
191+ def from_mij (mij ):
192+ """
193+ Converts from moment tensor (Up-South-East) to lune parameters
194+ This is a stripped out version based of mtpar's cmt2tt function by rmodrak
195+ It ONLY works with MTUQ convention.
196+
197+ Parameters:
198+ -----------
199+ mij : array_like, shape (6,)
200+ Moment tensor components in Up-South-East convention (default MTUQ)
201+ [Mxx, Myy, Mzz, Mxy, Mxz, Myz]
202+
203+ Returns:
204+ --------
205+ tuple : (rho, v, w, kappa, sigma, h)
206+ rho : Tape2012 magnitude parameter
207+ v : Tape2015 parameter v [-1/3, 1/3]
208+ w : Tape2015 parameter w [-3π/8, 3π/8]
209+ kappa : strike angle [0°, 360°]
210+ sigma : rake angle [-90°, 90°]
211+ h : cosine of dip angle [0, 1]
212+
213+ """
214+
215+ mij = np .array (mij )
216+
217+ # Cast from up-south-east to south-east-up convention (following mtpar)
218+ # USE convention: [Mxx, Myy, Mzz, Mxy, Mxz, Myz] (UP-SOUTH-EAST)
219+ # SEU convention: [Myy, Mzz, Mxx, Myz, Mxy, Mxz] (SOUTH-EAST-UP)
220+ mij_seu = np .array ([mij [1 ], mij [2 ], mij [0 ], mij [5 ], mij [3 ], mij [4 ]])
221+
222+ # Convert to matrix representation for eigenvalue decomposition
223+ M_seu = np .array ([[mij_seu [0 ], mij_seu [3 ], mij_seu [4 ]],
224+ [mij_seu [3 ], mij_seu [1 ], mij_seu [5 ]],
225+ [mij_seu [4 ], mij_seu [5 ], mij_seu [2 ]]])
226+
227+ # Eigenvalue decomposition (sort eigenvalues highest to lowest)
228+ lam , U = np .linalg .eigh (M_seu )
229+ idx = np .argsort (lam )[::- 1 ] # descending sort
230+ lam = lam [idx ]
231+ U = U [:, idx ]
232+
233+ # Convert eigenvalues to lune coordinates
234+ # magnitude of lambda vector
235+ lammag = np .linalg .norm (lam )
236+
237+ # seismic moment M0 = ||lam|| / sqrt(2)
238+ M0 = lammag / np .sqrt (2. )
239+ rho = M0 * np .sqrt (2. ) # rho parameter
240+
241+ # lune coordinates (gamma, delta)
242+ if np .sum (lam ) != 0. :
243+ bdot = np .sum (lam ) / (np .sqrt (3 ) * lammag )
244+ bdot = np .clip (bdot , - 1 , 1 ) # clipping to avoid numerical issues
245+ delta = 90. - np .rad2deg (np .arccos (bdot ))
246+ else :
247+ delta = 0.
248+
249+ # gamma coordinate
250+ if lam [0 ] != lam [2 ]:
251+ gamma = np .rad2deg (np .arctan ((- lam [0 ] + 2. * lam [1 ] - lam [2 ]) /
252+ (np .sqrt (3 ) * (lam [0 ] - lam [2 ]))))
253+ else :
254+ gamma = 0.
255+
256+ # Convert lune coordinates to v, w parameters
257+ gamma_rad = np .deg2rad (gamma )
258+ delta_rad = np .deg2rad (delta )
259+ beta = np .pi / 2. - delta_rad
260+
261+ v = (1. / 3. ) * np .sin (3. * gamma_rad )
262+ u = (0.75 * beta - 0.5 * np .sin (2. * beta ) + 0.0625 * np .sin (4. * beta ))
263+ w = 3. * np .pi / 8. - u
264+
265+ # Ensure det(U) = 1
266+ if np .linalg .det (U ) < 0 :
267+ U [:, 1 ] *= - 1
268+
269+ # 45° rotation around y axis to get the fault vectors from eigenvectors
270+ Y = np .array ([[np .cos (np .pi / 4 ), 0 , np .sin (np .pi / 4 )],
271+ [0 , 1 , 0 ],
272+ [- np .sin (np .pi / 4 ), 0 , np .cos (np .pi / 4 )]]) # rotmat(45, 1)
273+
274+ V = np .dot (U , Y )
275+ S = V [:, 0 ] # slip vector
276+ N = V [:, 2 ] # fault normal
277+
278+ # Round off small values to -/+1 and 0 (like in mtpar)
279+ EPSVAL = 1e-6
280+ S [np .abs (S ) < EPSVAL ] = 0
281+ N [np .abs (N ) < EPSVAL ] = 0
282+ S [np .abs (S - 1 ) < EPSVAL ] = 1
283+ S [np .abs (S + 1 ) < EPSVAL ] = - 1
284+ N [np .abs (N - 1 ) < EPSVAL ] = 1
285+ N [np .abs (N + 1 ) < EPSVAL ] = - 1
286+
287+ # Calculate fault angles using south-east-up basis
288+ zenith = np .array ([0 , 0 , 1 ])
289+ north = np .array ([- 1 , 0 , 0 ])
290+
291+ def faultvec2angles (S_vec , N_vec ):
292+ """Calculate fault angles from slip and normal vectors
293+ Similar to Ryan's mtpar implementation. I keep within the scope of
294+ the from_mij function because it is only useful there."""
295+ # Strike vector
296+ v_cross = np .cross (zenith , N_vec )
297+ if np .linalg .norm (v_cross ) == 0 :
298+ K = S_vec # horizontal fault case
299+ else :
300+ K = v_cross / np .linalg .norm (v_cross )
301+
302+ # Strike angle (kappa)
303+ def fangle_signed (va , vb , vnor ):
304+ # Angle between two vectors with sign
305+ xy = np .dot (va , vb )
306+ xx = np .dot (va , va )
307+ yy = np .dot (vb , vb )
308+ theta = np .rad2deg (np .arccos (np .clip (xy / (xx * yy )** 0.5 , - 1 , 1 )))
309+
310+ if abs (theta - 180 ) <= EPSVAL :
311+ return 180
312+ else :
313+ Dmat = np .column_stack ([va , vb , vnor ])
314+ if np .linalg .det (Dmat ) < 0 :
315+ return - theta
316+ else :
317+ return theta
318+
319+ kappa = fangle_signed (north , K , - zenith )
320+ kappa = kappa % 360. # wrap to [0, 360)
321+
322+ # Dip angle (theta)
323+ costh = np .dot (N_vec , zenith )
324+ theta = np .rad2deg (np .arccos (np .clip (costh , - 1 , 1 )))
325+
326+ # Rake angle (sigma)
327+ sigma = fangle_signed (K , S_vec , N_vec )
328+
329+ return theta , sigma , kappa , K
330+
331+ # Frame2angles: evaluate four combinations to resolve ambiguity
332+ # There are four combinations of N and S that represent a double couple
333+ # moment tensor (TT2012, Fig. 15). We need to find the one within the
334+ # proper bounding region (TT2012, Figs. 16, B1)
335+
336+ # Four combinations for the given frame
337+ S1 , N1 = S , N
338+ S2 , N2 = - S , - N
339+ S3 , N3 = N , S
340+ S4 , N4 = - N , - S
341+
342+ # Calculate fault angles for each combination
343+ theta1 , sigma1 , kappa1 , K1 = faultvec2angles (S1 , N1 )
344+ theta2 , sigma2 , kappa2 , K2 = faultvec2angles (S2 , N2 )
345+ theta3 , sigma3 , kappa3 , K3 = faultvec2angles (S3 , N3 )
346+ theta4 , sigma4 , kappa4 , K4 = faultvec2angles (S4 , N4 )
347+
348+ theta = np .array ([theta1 , theta2 , theta3 , theta4 ])
349+ sigma = np .array ([sigma1 , sigma2 , sigma3 , sigma4 ])
350+ kappa = np .array ([kappa1 , kappa2 , kappa3 , kappa4 ])
351+
352+ # Which combination lies within the bounding region?
353+ btheta = (theta <= 90. + EPSVAL )
354+ bsigma = (np .abs (sigma ) <= 90. + EPSVAL )
355+ bb = np .logical_and (btheta , bsigma )
356+ ii = np .where (bb )[0 ]
357+ nn = len (ii )
358+
359+ if nn == 0 :
360+ raise Exception ('No valid fault plane found within bounding region' )
361+ elif nn == 1 :
362+ jj = ii [0 ]
363+ elif nn == 2 :
364+ # Choose one of the two based on strike angle
365+ # This is a simplified version of the _pick function in mtpar
366+ if kappa [ii [0 ]] < 180 :
367+ jj = ii [0 ]
368+ else :
369+ jj = ii [1 ]
370+ else :
371+ # Take the first one for unusual cases
372+ jj = ii [0 ]
373+
374+ # Select the angles from the chosen combination
375+ final_theta = theta [jj ]
376+ final_sigma = sigma [jj ]
377+ final_kappa = kappa [jj ]
378+
379+ # Convert theta to h parameter
380+ h = np .cos (np .deg2rad (final_theta ))
381+
382+ return (rho , v , w , final_kappa , final_sigma , h )
383+
384+
191385def to_xyz (F0 , phi , h ):
192386 """ Converts from spherical to Cartesian coordinates (east-north-up)
193387 """
0 commit comments