pySATSI 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pySATSI/__init__.py ADDED
@@ -0,0 +1 @@
1
+ import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
@@ -0,0 +1 @@
1
+ import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
@@ -0,0 +1,481 @@
1
+ '''
2
+ Functions used for bootstrapping and uncertainty computations.
3
+
4
+ Author: Robert J. Skoumal (rskoumal@usgs.gov)
5
+ '''
6
+
7
+ # Standard libraries
8
+ import multiprocessing
9
+
10
+ # External libraries
11
+ import pandas as pd
12
+ import numpy as np
13
+ import scipy.stats as st
14
+ rng=np.random.default_rng(123) # Used to produce reproducable bootstrapped results.
15
+
16
+ # Local libraries
17
+ import functions.stress as stress
18
+ import functions.fun as fun
19
+ import functions.knee as knee
20
+
21
+ def compute_boot(mech_df,damp_df,p_dict):
22
+ '''
23
+ nboot: Number of bootstrap iterations to perform.
24
+ For nboot==1 or the first boostrap iteration, it will compute the stresses using the provided nodal planes and events.
25
+ For nboot>1, it will randomize according to the $randomize_events and $randomize_nodalplane options.
26
+ fracplane: Chance the nodal plane is the fault plane. Either a float or an array of floats
27
+ '''
28
+
29
+ num_iterations=p_dict['niterations']
30
+ if num_iterations<1:
31
+ num_iterations=1
32
+
33
+ for iter_x in range(num_iterations):
34
+ boot_stress_df=[]
35
+ if p_dict['num_cpus']==1: # Runs bootstrapping in serial
36
+ for boot_x in range(p_dict['nboot']):
37
+ select_mech_df=mech_df.copy()
38
+ if boot_x!=0:
39
+ select_mech_df=randomize_mech(select_mech_df,p_dict)
40
+ boot_stress_df.append(stress.slickenside(select_mech_df,damp_df,p_dict['damp_param'],p_dict,boot_x=boot_x,iter_x=iter_x))
41
+ else: # Runs bootstrapping in parallel
42
+ print('\tComputing bootstraps in parallel...')
43
+ pool=multiprocessing.Pool(processes=p_dict['num_cpus'])
44
+ async_results=[]
45
+ for boot_x in range(p_dict['nboot']):
46
+ select_mech_df=mech_df.copy()
47
+ if boot_x!=0:
48
+ select_mech_df=randomize_mech(select_mech_df,p_dict)
49
+ async_results.append(pool.apply_async(stress.slickenside,
50
+ (select_mech_df,damp_df,p_dict['damp_param'],p_dict),dict(boot_x=boot_x,iter_x=iter_x)))
51
+ pool.close()
52
+ pool.join()
53
+ for result in async_results:
54
+ boot_stress_df.append(result.get())
55
+
56
+ boot_stress_df=pd.concat(boot_stress_df).reset_index(drop=True)
57
+
58
+ # Use fault instability to inform fault plane
59
+ if p_dict['niterations']>0:
60
+ if (iter_x>=(num_iterations-2)) and (p_dict['final_instability']==True):
61
+ instability_weight=1.
62
+ iteration_weight=1.
63
+ damp_df=pd.DataFrame(columns=['cluster_id1','cluster_id2'])
64
+ else:
65
+ instability_weight=p_dict['instability_weight']
66
+ iteration_weight=p_dict['iteration_weight']
67
+
68
+ mech_df['instability'],mech_df['aux_instability']=fault_instability(boot_stress_df,mech_df,p_dict)
69
+ Idiff=mech_df['instability'].values**2-mech_df['aux_instability'].values**2
70
+ abs_Idiff=np.abs(Idiff)
71
+
72
+ # Determine the fault probability
73
+ old_fault_probability=mech_df['fault_probability'].values
74
+ mech_df['fault_probability']=100*(iteration_weight*
75
+ (((Idiff/(instability_weight*abs_Idiff-instability_weight+1)+1)/2)-old_fault_probability/100) +
76
+ old_fault_probability/100)
77
+
78
+ # Checks for convergence
79
+ mean_prob_diff=np.mean(np.abs(mech_df['fault_probability'].values-old_fault_probability))
80
+ print('\t\tMean change of fault probability: {}%'.format(mean_prob_diff.round(2)))
81
+ if mean_prob_diff<p_dict['convergence_threshold']:
82
+ if iter_x<(num_iterations-1):
83
+ print('\tReached the convergence threshold of {}%. Stopping iterations.'.format(p_dict['convergence_threshold']))
84
+ break
85
+ if p_dict['iterative_damping']:
86
+ if iter_x<(num_iterations-1):
87
+ iteration_damp_misfit_df,p_dict['damp_param']=knee.find_damp(mech_df,damp_df,p_dict)
88
+ if p_dict['iterative_damping']:
89
+ return boot_stress_df,iteration_damp_misfit_df
90
+ else:
91
+ return boot_stress_df,None
92
+
93
+ def randomize_mech(mech_df,p_dict):
94
+ '''
95
+ Randomizes the nodal plane for the mechanisms
96
+
97
+ Input:
98
+ mech_df: DataFrame of [ddir, ddir, dip, dip2, rake1, rake2, cluster_id]
99
+ fracplane: Percentage that the nodal plane is the fault plane. Either a float or an array of floats
100
+ randomize_events: Whether the selection of events are randomized, boolean.
101
+ randomize_nodalplane: Whether the selection of nodal planes are randomized, boolean.
102
+ Output:
103
+ select_mech_df: DataFrame containing randomized nodal planes
104
+ '''
105
+ random_mech_df=mech_df.copy()
106
+ nobs=len(random_mech_df)
107
+ if p_dict['randomize_events']: # Randomizes which events are selected
108
+ rand1=rng.choice(nobs,nobs)
109
+ random_mech_df=random_mech_df.copy().loc[rand1,:].reset_index(drop=True)
110
+
111
+ # Applies noise and randomizes which nodal plane is the fault plane
112
+ if random_mech_df['fault_plane_uncertainty'].any():
113
+ random_mech_df=mech_noise(random_mech_df)
114
+ elif p_dict['randomize_nodalplane']:
115
+ random_mech_df['ddir2'],random_mech_df['dip2'],random_mech_df['rake2']=fun.aux_plane_ddir(random_mech_df['ddir'].values,random_mech_df['dip'].values,random_mech_df['rake'].values)
116
+ rand2=rng.uniform(size=nobs)*100
117
+ if 'fault_probability' in random_mech_df:
118
+ ind_rand2=np.where(rand2>random_mech_df['fault_probability'])[0]
119
+ else:
120
+ ind_rand2=np.where(rand2>p_dict['fracplane'])[0]
121
+ random_mech_df.loc[ind_rand2,['ddir','dip','rake']]=random_mech_df.loc[ind_rand2,['ddir2','dip2','rake2']].values
122
+ random_mech_df=random_mech_df.drop(columns=['ddir2','dip2','rake2'])
123
+
124
+ return random_mech_df
125
+
126
+
127
+ def avguncert_azimuth_plunge(azimuth,plunge,round=1,confidence_thresh=95.):
128
+ '''
129
+ Given azimuths and plunges (in degrees), computes the eigenvectors of the
130
+ covariance matrix and returns the mean azimuth in plunge (in degrees).
131
+ '''
132
+
133
+ # Converts plunge,azimuth (degrees) to lon,lat (radians)
134
+ lat=np.radians(90-plunge)
135
+ lon=0
136
+ theta=np.radians(azimuth)
137
+
138
+ # Converts lon,lat to xyz
139
+ x=np.cos(lat)*np.cos(lon)
140
+ y=np.cos(lat)*np.sin(lon)
141
+ z=np.sin(lat)
142
+
143
+ # Rotate lon,lat coords by theta degrees
144
+ y_2=y*np.cos(theta)+z*np.sin(theta)
145
+ z_2=-y*np.sin(theta)+z*np.cos(theta)
146
+ r=np.sqrt(x**2+y_2**2+z_2**2)
147
+ lat=np.arcsin(z_2/r)
148
+ lon=np.arctan2(y_2,x)
149
+
150
+ # Computes the antipodes
151
+ x=np.cos(lat)*np.cos(lon)
152
+ y=np.cos(lat)*np.sin(lon)
153
+ z=np.sin(lat)
154
+ r=np.sqrt(x**2+y**2+z**2)
155
+ lat_antipode=np.arcsin(-z/r)
156
+ lon_antipode=np.arctan2(-y,-x)
157
+
158
+ # Concats the antipodes to the lon,lat arrays
159
+ lon,lat=np.hstack([lon,lon_antipode]),np.hstack([lat,lat_antipode])
160
+ xyz=np.zeros((len(lat),3))
161
+ xyz[:,0]=np.cos(lat)*np.cos(lon)
162
+ xyz[:,1]=np.cos(lat)*np.sin(lon)
163
+ xyz[:,2]=np.sin(lat)
164
+
165
+ # Computes eigenvectors
166
+ cov=np.cov(xyz.T)
167
+ eigvalues,eigvectors=np.linalg.eigh(cov)
168
+ sort_index=eigvalues.argsort()
169
+ vecs=eigvectors[:,sort_index]
170
+ x,y,z=vecs[:,-1]
171
+
172
+ # Calculate azimuth and plunge (in radians)
173
+ azimuth_rads=np.arctan2(z,y)
174
+ r=np.sqrt(x**2+y**2+z**2)
175
+ if r==0:
176
+ plunge_rads=np.arcsin(1)
177
+ else:
178
+ plunge_rads=np.arcsin(x/r)
179
+
180
+ # Convert azimuth and plunge to degrees
181
+ mean_plunge=np.degrees(plunge_rads)
182
+ mean_azimuth=np.degrees(azimuth_rads)
183
+ mean_azimuth=90-mean_azimuth
184
+ if mean_azimuth<0:
185
+ mean_azimuth+=360
186
+
187
+ # If upwards plunge, get antipode
188
+ if mean_plunge<0:
189
+ mean_plunge*=-1
190
+ mean_azimuth-=180
191
+ if mean_azimuth<0:
192
+ mean_azimuth+=360
193
+
194
+ mean_azimuth=np.round(mean_azimuth,round)
195
+ mean_plunge=np.round(mean_plunge,round)
196
+
197
+ # Calculate confidence bounds
198
+ n_measurements=len(azimuth)
199
+ j=int(np.floor((n_measurements-1)*((100.-confidence_thresh)/100.)))
200
+ dot_conf=np.dot(xyz[:int(xyz.shape[0]/2)],np.asarray([x,y,z]))
201
+ ind_conf=np.argsort(np.abs(dot_conf))[j]
202
+ conf_azimuth=azimuth[ind_conf]
203
+ conf_plunge=plunge[ind_conf]
204
+
205
+ if dot_conf[ind_conf]<0:
206
+ conf_azimuth-=180
207
+
208
+ uncert_azimuth=fun.az_diff_180(mean_azimuth,conf_azimuth)
209
+ uncert_plunge=np.abs(mean_plunge-conf_plunge)
210
+
211
+ # Round uncertainties
212
+ uncert_azimuth=np.round(uncert_azimuth,round)
213
+ uncert_plunge=np.round(uncert_plunge,round)
214
+
215
+ return mean_azimuth,mean_plunge,uncert_azimuth,uncert_plunge
216
+
217
+ def uncert(boot_stress_df,p_dict):
218
+ '''
219
+ Calculates the average and variability range of the bootstrapped results
220
+ '''
221
+ # Group bootstrapped results by cluster_id
222
+ boot_stress_df=boot_stress_df.sort_values(by=['cluster_id','boot'])
223
+
224
+ # If no bootstrapping was done, no uncertainties need to be calculated
225
+ if boot_stress_df['boot'].max()==0:
226
+ boot_uncert_df=boot_stress_df[['cluster_id','phi','az1','pl1','az2','pl2','az3','pl3',]]
227
+ else: # Calculates uncertainties
228
+ group_boot_stress_df=boot_stress_df.groupby(by='cluster_id')
229
+ r_keys=list(group_boot_stress_df.groups.keys())
230
+
231
+ # Create empty uncert dataframe
232
+ boot_uncert_df=pd.DataFrame(index=np.arange(len(r_keys)),
233
+ columns=['cluster_id','phi','uncert_phi',
234
+ 'az1','az1_uncert','pl1','pl1_uncert',
235
+ 'az2','az2_uncert','pl2','pl2_uncert',
236
+ 'az3','az3_uncert','pl3','pl3_uncert'],
237
+ dtype=float)
238
+ boot_uncert_df['cluster_id']=r_keys
239
+ boot_uncert_df['cluster_id']=boot_uncert_df['cluster_id'].astype(int)
240
+
241
+ # Loop over each cluster
242
+ for r_x,r_key in enumerate(r_keys):
243
+ stress_df=group_boot_stress_df.get_group(r_key)
244
+
245
+ # If there's only one record, no need to calculate uncertainties
246
+ if len(stress_df)==1:
247
+ boot_uncert_df.loc[r_x,['phi','az1','pl1','az2','pl2','az3','pl3']]=stress_df.iloc[0][['phi','az1','pl1','az2','pl2','az3','pl3']]
248
+ continue
249
+
250
+ # Computes mean azimuth/plunge using the eigenvectors of the covariance matrix
251
+ boot_uncert_df.loc[r_x,['az1','pl1','az1_uncert','pl1_uncert']]=avguncert_azimuth_plunge(
252
+ stress_df['az1'].values,stress_df['pl1'].values,
253
+ round=p_dict['angle_precision'],confidence_thresh=p_dict['confidence'])
254
+ boot_uncert_df.loc[r_x,['az2','pl2','az2_uncert','pl2_uncert']]=avguncert_azimuth_plunge(
255
+ stress_df['az2'].values,stress_df['pl2'].values,
256
+ round=p_dict['angle_precision'],confidence_thresh=p_dict['confidence'])
257
+ boot_uncert_df.loc[r_x,['az3','pl3','az3_uncert','pl3_uncert']]=avguncert_azimuth_plunge(
258
+ stress_df['az3'].values,stress_df['pl3'].values,
259
+ round=p_dict['angle_precision'],confidence_thresh=p_dict['confidence'])
260
+
261
+ # Computes phi stats
262
+ boot_uncert_df.loc[r_x,'phi']=np.round(stress_df['phi'].mean(),p_dict['vector_precision'])
263
+ if len(stress_df['phi'].unique())>1:
264
+ tmp_interval=st.t.interval(p_dict['confidence']/100,stress_df.shape[0]-1,loc=boot_uncert_df.loc[r_x,['phi']],scale=st.sem(stress_df['phi'].values))
265
+ boot_uncert_df.loc[r_x,'uncert_phi']=np.round(np.max(np.abs(boot_uncert_df.loc[r_x,'phi']-tmp_interval)),p_dict['vector_precision'])
266
+ else:
267
+ boot_uncert_df.loc[r_x,'uncert_phi']=0.
268
+
269
+ return boot_uncert_df
270
+
271
+ def mech_noise(select_mech_df):
272
+ '''
273
+ Adds random noise to the mechanism solutions
274
+ '''
275
+ noise_mech_df=select_mech_df.copy()
276
+ strike_r=np.radians(noise_mech_df['ddir'].values-90)
277
+ dip_r=np.radians(noise_mech_df['dip'].values)
278
+ rake_r=np.radians(noise_mech_df['rake'].values)
279
+
280
+ num_mechs=len(strike_r)
281
+
282
+ # Fault normals
283
+ n=np.zeros((num_mechs,3))
284
+ n[:,0]=-np.sin(dip_r)*np.sin(strike_r)
285
+ n[:,1]=np.sin(dip_r)*np.cos(strike_r)
286
+ n[:,2]=-np.cos(dip_r)
287
+
288
+ # Slip direction
289
+ u=np.zeros((num_mechs,3))
290
+ u[:,0]=np.cos(rake_r)*np.cos(strike_r) + np.cos(dip_r)*np.sin(rake_r)*np.sin(strike_r)
291
+ u[:,1]=np.cos(rake_r)*np.sin(strike_r) - np.cos(dip_r)*np.sin(rake_r)*np.cos(strike_r)
292
+ u[:,2]=-np.sin(rake_r)*np.sin(dip_r)
293
+
294
+ rand2=rng.uniform(size=num_mechs)*100
295
+ if 'fault_probability' in noise_mech_df:
296
+ tmp_bool=rand2<noise_mech_df['fault_probability'].values
297
+ else:
298
+ tmp_bool=rand2<50
299
+ i0_ind=np.where(tmp_bool)[0]
300
+ i1_ind=np.where(~tmp_bool)[0]
301
+
302
+ b=np.zeros((len(strike_r),3))
303
+ b[i0_ind]=np.cross(n[i0_ind],u[i0_ind])
304
+ b[i1_ind]=np.cross(n[i1_ind],u[i1_ind])
305
+
306
+ fault_plane_uncertainty=noise_mech_df['fault_plane_uncertainty'].values
307
+ if 'aux_plane_uncertainty' in noise_mech_df.columns:
308
+ fault_plane_uncertainty[i1_ind]=noise_mech_df['aux_plane_uncertainty'].values[i1_ind]
309
+
310
+ # Random fault normal
311
+ n_deviation_random=np.radians(fault_plane_uncertainty*np.random.randn(num_mechs))
312
+ b_deviation_random=np.radians(fault_plane_uncertainty*np.random.randn(num_mechs))
313
+
314
+ # Random noisy azimuth
315
+ n_azimuth_random=np.pi*2*np.random.random(num_mechs)
316
+ b_azimuth_random=np.pi*2*np.random.random(num_mechs)
317
+
318
+ # Applies random noise to n
319
+ n_perpendicular=u*np.sin(n_azimuth_random)[:,np.newaxis]+b*np.cos(n_azimuth_random)[:,np.newaxis]
320
+ n_noise_component=n_perpendicular*np.sin(n_deviation_random)[:,np.newaxis]
321
+ n_noisy=n+n_noise_component
322
+ n_noisy=n_noisy/np.linalg.norm(n_noisy,2,axis=1)[:,np.newaxis]
323
+
324
+ # Applies random noise to b
325
+ b_perpendicular=n_noisy*np.sin(b_azimuth_random)[:,np.newaxis]+u*np.cos(b_azimuth_random)[:,np.newaxis]
326
+ b_noise_component=b_perpendicular*np.sin(b_deviation_random)[:,np.newaxis]
327
+ b_noisy=b+b_noise_component
328
+ b_noisy=b_noisy/np.linalg.norm(b_noisy,2,axis=1)[:,np.newaxis]
329
+
330
+ # Determines noisy slip direction
331
+ u_noisy=np.cross(n_noisy,b_noisy)
332
+ u_noisy*=np.sign(np.sum(u*u_noisy,axis=1))[:,np.newaxis]
333
+
334
+ n=n_noisy.copy()
335
+ u=u_noisy.copy()
336
+ n[i1_ind]=u_noisy[i1_ind]
337
+ u[i1_ind]=n_noisy[i1_ind]
338
+
339
+ n1=n.copy()
340
+ u1=u.copy()
341
+ n2=u.copy()
342
+ u2=n.copy()
343
+
344
+ # Ensures vertical components are negative
345
+ pos_n1_ind=np.where(n1[:,2]>0)[0]
346
+ n1[pos_n1_ind]*=-1
347
+ u1[pos_n1_ind]*=-1
348
+ pos_n2_ind=np.where(n2[:,2]>0)[0]
349
+ n2[pos_n2_ind]*=-1
350
+ u2[pos_n2_ind]*=-1
351
+
352
+ # Determine strike, dip, rake of noisy mech
353
+ r_dip=np.arccos(-n1[:,2])
354
+ r_strike=np.arcsin(-n1[:,0]/np.sqrt(n1[:,0]**2+n1[:,1]**2))
355
+ dip=np.degrees(r_dip)
356
+ strike=np.degrees(r_strike)
357
+ rake=np.degrees(np.arcsin(-u1[:,2]/np.sin(r_dip)))
358
+
359
+ # Determining quadrants
360
+ n1_quad_ind=np.where(n1[:,1]<0)[0]
361
+ strike[n1_quad_ind]=180-strike[n1_quad_ind]
362
+ cos_rake=u1[:,0]*np.cos(np.radians(strike))+u1[:,1]*np.sin(np.radians(strike))
363
+ u1_quad_ind=np.where(cos_rake<0)[0]
364
+ rake[u1_quad_ind]=180-rake[u1_quad_ind]
365
+
366
+ strike[strike<0]+=360
367
+ rake[rake<-180]+=360
368
+ rake[rake>180]-=360
369
+
370
+ noise_mech_df['ddir']=(strike+90)%360
371
+ noise_mech_df['dip']=dip
372
+ noise_mech_df['rake']=rake
373
+
374
+ return noise_mech_df
375
+
376
+ def fault_instability(boot_stress_df,mech_df,p_dict):
377
+ '''
378
+ Calculates the fault instability for the primary and auxiliary
379
+ nodal planes following the instability parameter of Vavryčuk (2014).
380
+ Takes the median instability considering the provided friction values.
381
+ '''
382
+ group_mech_df=mech_df.groupby(by='cluster_id')
383
+ group_boot_stress_df=boot_stress_df.groupby(by='cluster_id')
384
+ instability=np.zeros(mech_df.shape[0]);instability[:]=np.nan
385
+ aux_instability=np.zeros(mech_df.shape[0]);aux_instability[:]=np.nan
386
+
387
+ for cluster_id in list(group_mech_df.groups.keys()):
388
+ cluster_mech_df=group_mech_df.get_group(cluster_id)
389
+ cluster_boot_stress_df=group_boot_stress_df.get_group(cluster_id)
390
+
391
+ ddir=cluster_mech_df['ddir'].values
392
+ dip=cluster_mech_df['dip'].values
393
+ rake=cluster_mech_df['rake'].values
394
+
395
+ # Determines aux nodal plane
396
+ aux_ddir,aux_dip,aux_rake=fun.aux_plane_ddir(ddir,dip,rake)
397
+
398
+ # Converts ddir,dip to radians
399
+ z=np.radians(ddir)
400
+ dip[dip==90]=89.99999
401
+ z2=np.radians(dip)
402
+
403
+ # Converts aux ddir,dip to radians
404
+ aux_z=np.radians(aux_ddir)
405
+ aux_ddir[aux_ddir==90]=89.99999
406
+ aux_z2=np.radians(aux_dip)
407
+
408
+ # Normal vector to plane
409
+ normal=np.zeros((len(z),3))
410
+ normal[:,0]=np.sin(z)*np.sin(z2)
411
+ normal[:,1]=np.cos(z)*np.sin(z2)
412
+ normal[:,2]=np.cos(z2)
413
+
414
+ # Normal vector to aux plane
415
+ aux_normal=np.zeros((len(aux_z),3))
416
+ aux_normal[:,0]=np.sin(aux_z)*np.sin(aux_z2)
417
+ aux_normal[:,1]=np.cos(aux_z)*np.sin(aux_z2)
418
+ aux_normal[:,2]=np.cos(aux_z2)
419
+
420
+ # Determines stress tensor
421
+ tau=np.zeros((len(cluster_boot_stress_df),3,3))
422
+ tau[:,0,0]=cluster_boot_stress_df.loc[:,'st00']
423
+ tau[:,0,1]=cluster_boot_stress_df.loc[:,'st01']
424
+ tau[:,0,2]=cluster_boot_stress_df.loc[:,'st02']
425
+ tau[:,1,1]=cluster_boot_stress_df.loc[:,'st11']
426
+ tau[:,1,2]=cluster_boot_stress_df.loc[:,'st12']
427
+ tau[:,2,2]=cluster_boot_stress_df.loc[:,'st22']
428
+ tau[:,1,0]=tau[:,0,1]
429
+ tau[:,2,0]=tau[:,0,2]
430
+ tau[:,2,1]=tau[:,1,2]
431
+ tau=np.sum(tau,axis=0)
432
+ tau=tau/np.linalg.norm(tau,2)
433
+ tau_rot=tau.copy()
434
+ tau_rot[0,0]=tau[1,1]
435
+ tau_rot[1,1]=tau[0,0]
436
+ tau_rot[0,2]=tau[1,2]*-1
437
+ tau_rot[1,2]=tau[0,2]*-1
438
+ tau_rot[1,0]=tau_rot[0,1]
439
+ tau_rot[2,0]=tau_rot[0,2]
440
+ tau_rot[2,1]=tau_rot[1,2]
441
+ tau=tau_rot.copy()
442
+
443
+ lam,vecs=np.linalg.eig(tau)
444
+
445
+ # Sort eigenvalues
446
+ sort_ind=lam.argsort()
447
+ lam=lam[sort_ind]
448
+ vecs=vecs[:,sort_ind]
449
+
450
+ # Shape ratio
451
+ shape_ratio=(lam[0]-lam[1])/(lam[0]-lam[2])
452
+
453
+ normal=normal[:,[1,0,2]]
454
+ normal[:,2]*=-1
455
+ aux_normal=aux_normal[:,[1,0,2]]
456
+ aux_normal[:,2]*=-1
457
+
458
+ # Fault and aux plane normals * principal stresses
459
+ n=np.sum(normal[:,:,np.newaxis]*vecs,axis=1)
460
+ aux_n=np.sum(aux_normal[:,:,np.newaxis]*vecs,axis=1)
461
+
462
+ # Shear and normal traction along plane (Vavryčuk EQ 17,18)
463
+ tau_shear=np.sqrt(n[:,0]**2+(1-2*shape_ratio)**2*n[:,1]**2.+n[:,2]**2-(n[:,0]**2+(1-2*shape_ratio)*n[:,1]**2-n[:,2]**2)**2)
464
+ tau_normal=(n[:,0]**2+(1-2*shape_ratio)*n[:,1]**2-n[:,2]**2)
465
+
466
+ # Shear and normal traction along aux plane (Vavryčuk EQ 17,18)
467
+ aux_tau_shear=np.sqrt(aux_n[:,0]**2+(1-2*shape_ratio)**2*aux_n[:,1]**2.+aux_n[:,2]**2-(aux_n[:,0]**2+(1-2*shape_ratio)*aux_n[:,1]**2-aux_n[:,2]**2)**2)
468
+ aux_tau_normal=(aux_n[:,0]**2+(1-2*shape_ratio)*aux_n[:,1]**2-aux_n[:,2]**2)
469
+
470
+ # Computes instability for plane (Vavryčuk EQ 16)
471
+ cluster_instability=(tau_shear[:,np.newaxis]-p_dict['friction']*(tau_normal[:,np.newaxis]-1))/(p_dict['friction']+np.sqrt(1+p_dict['friction']**2))
472
+ cluster_instability=np.median(cluster_instability,axis=1)
473
+
474
+ # Computes instability for aux plane (Vavryčuk EQ 16)
475
+ cluster_aux_instability=(aux_tau_shear[:,np.newaxis]-p_dict['friction']*(aux_tau_normal[:,np.newaxis]-1))/(p_dict['friction']+np.sqrt(1+p_dict['friction']**2))
476
+ cluster_aux_instability=np.median(cluster_aux_instability,axis=1)
477
+
478
+ instability[cluster_mech_df.index]=cluster_instability
479
+ aux_instability[cluster_mech_df.index]=cluster_aux_instability
480
+
481
+ return instability, aux_instability
@@ -0,0 +1,115 @@
1
+ '''
2
+ Functions used for creating synthetic checkerboards.
3
+
4
+ Author: Robert J. Skoumal (rskoumal@usgs.gov)
5
+ '''
6
+
7
+ # External libraries
8
+ import numpy as np
9
+
10
+ def synth_rake(strike,dip,tau):
11
+ '''
12
+ Computes synthetic rakes given a tau value and catalog of strike,dip.
13
+ '''
14
+ num_events=len(strike)
15
+ strike_radians=np.radians(strike)
16
+ dip_radians=np.radians(dip)
17
+ dip_radians[dip_radians==0]=0.00001
18
+
19
+ # Fault normals
20
+ n=np.zeros((num_events,3))
21
+ n[:,0]=-np.sin(dip_radians)*np.sin(strike_radians)
22
+ n[:,1]=np.sin(dip_radians)*np.cos(strike_radians)
23
+ n[:,2]=-np.cos(dip_radians)
24
+
25
+ term1=np.zeros((num_events,3))
26
+ term2=np.zeros((num_events,3))
27
+ shear_traction=np.zeros((num_events,3))
28
+ shear_trac_1=np.zeros((num_events))
29
+ shear_trac_2=np.zeros((num_events))
30
+ shear_trac_3=np.zeros((num_events))
31
+
32
+ for k in range(0,num_events):
33
+ for i in range(0,3):
34
+ for j in range(0,3):
35
+ term1[k,i]=term1[k,i]+tau[i,j]*n[k,j]
36
+ for l in range(0,3):
37
+ term2[k,i]=term2[k,i]+tau[j,l]*n[k,j]*n[k,l]*n[k,i]
38
+ shear_traction[k,i]=term1[k,i]-term2[k,i]
39
+ shear_traction[(shear_traction==0).all(axis=1),2]=0.0000001
40
+
41
+ # Normalization
42
+ for k in range(0,num_events):
43
+ shear_trac_1[k]=shear_traction[k,0]/np.linalg.norm(shear_traction[k,:],ord=2)
44
+ shear_trac_2[k]=shear_traction[k,1]/np.linalg.norm(shear_traction[k,:],ord=2)
45
+ shear_trac_3[k]=shear_traction[k,2]/np.linalg.norm(shear_traction[k,:],ord=2)
46
+
47
+ tmp=-shear_trac_3/np.sin(dip_radians)
48
+ tmp[tmp>1]=1
49
+ tmp[tmp<-1]=-1
50
+ synth_rake=np.degrees(np.arcsin(tmp))
51
+
52
+ # Determining the quadrant
53
+ cos_rake=shear_trac_1*np.cos(strike_radians)+shear_trac_2*np.sin(strike_radians)
54
+ tmp_flag=cos_rake<0
55
+ synth_rake[tmp_flag]=180-synth_rake[tmp_flag]
56
+ synth_rake[synth_rake<-180]+=360
57
+ synth_rake[synth_rake>180]-=360
58
+ synth_rake=np.round(synth_rake,2)
59
+
60
+ return synth_rake
61
+
62
+ def determine_stress_tensor(shape_ratio,sigma1_az,sigma2_az,theta):
63
+ '''
64
+ Based on code written by Patricia Martínez-Garzón. For more info, see:
65
+ Martínez-Garzón, P., Heidbach, O., & Bohnhoff, M. (2020). Contemporary stress
66
+ and strain field in the Mediterranean from stress inversion of focal mechanisms
67
+ and GPS data. Tectonophysics, 774, 228286.
68
+ '''
69
+ sigma1_az=sigma1_az%360
70
+ sigma2_az=sigma2_az%360
71
+
72
+ theta_radians=np.radians(90-theta)
73
+ sigma1_az_radians=np.radians(sigma1_az)
74
+ sigma2_az_radians=np.radians(sigma2_az)
75
+
76
+ # Calculate deviatoric stress magnitudes
77
+ sigma_1=-1
78
+ sigma_2=sigma_1*(1-2*shape_ratio)/(1+shape_ratio)
79
+ sigma_3=-sigma_1-sigma_2
80
+
81
+ # Normalize
82
+ sigma_dev=sigma_3-sigma_1
83
+ sigma_1=2*sigma_1/sigma_dev
84
+ sigma_2=2*sigma_2/sigma_dev
85
+ sigma_3=2*sigma_3/sigma_dev
86
+
87
+ sigma_1=sigma_1-sigma_3
88
+ sigma_2=sigma_2-sigma_3
89
+ sigma_3=0
90
+
91
+ # Calculate rotated eigenvectors S1, S2, S3 and deviatoric stress tensor (tau)
92
+ sigma_vector_1=np.zeros((3,1))
93
+ sigma_vector_2=np.zeros((3,1))
94
+ sigma_vector_3=np.zeros((3,1))
95
+
96
+ sigma_vector_1[0]=np.sin(theta_radians)*np.cos(sigma1_az_radians)
97
+ sigma_vector_1[1]=np.sin(theta_radians)*np.sin(sigma1_az_radians)
98
+ sigma_vector_1[2]=np.cos(theta_radians)
99
+
100
+ sigma_vector_2[0]=-(np.cos(sigma1_az_radians)*np.cos(sigma2_az_radians)*np.cos(theta_radians)+\
101
+ np.sin(sigma1_az_radians)*np.sin(sigma2_az_radians))
102
+ sigma_vector_2[1]=np.cos(sigma1_az_radians)*np.sin(sigma2_az_radians)-\
103
+ np.cos(sigma2_az_radians)*np.cos(theta_radians)*np.sin(sigma1_az_radians)
104
+ sigma_vector_2[2]=np.cos(sigma2_az_radians)*np.sin(theta_radians)
105
+
106
+ sigma_vector_3[0]=np.cos(sigma1_az_radians)*np.cos(theta_radians)*np.sin(sigma2_az_radians)-\
107
+ np.cos(sigma2_az_radians)*np.sin(sigma1_az_radians)
108
+ sigma_vector_3[1]=np.cos(sigma1_az_radians)*np.cos(sigma2_az_radians)+\
109
+ np.cos(theta_radians)*np.sin(sigma1_az_radians)*np.sin(sigma2_az_radians)
110
+ sigma_vector_3[2]=-np.sin(sigma2_az_radians)*np.sin(theta_radians)
111
+
112
+ tau=sigma_1*sigma_vector_1*sigma_vector_1.T+sigma_2*sigma_vector_2*sigma_vector_2.T+\
113
+ sigma_3*sigma_vector_3*sigma_vector_3.T
114
+
115
+ return tau