/
APWPKit.py
369 lines (327 loc) · 13.6 KB
/
APWPKit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
import numpy as np
import pandas as pd
import PlateRotKit as rotkit
import pmag, pmagplotlib
from mpl_toolkits.basemap import Basemap
#adds a new ellipse function to Basemap class
def ellipse(self, lon, lat, a, b, az, **kwargs):
"""
Draws an ellipse on the Earth's surface, centered at lon, lat, with semi-major-axis a and
semi-minor axis b (in degrees) with semi-major axis oriented at az degrees wrt N
Ellipse coordinates generated by call to fortran routine make_ell
Other \**kwargs passed on to matplotlib.patches.Polygon
RETURNS
poly : a maptplotlib.patches.Polygon object.
"""
ax = self._check_ax()
ell_lats,ell_lons=rotkit_f.make_ell(a,b,az,lat,lon)
x, y = self(ell_lons, ell_lats)
poly = Polygon(zip(x,y), **kwargs)
ax.add_patch(poly)
# Set axes limits to fit map region.
self.set_axes_limits(ax=ax)
return poly
Basemap.ellipse = ellipse
def get_VGPs(backwardsrots):
"""
Calculate the co-ordinates for the APWP given a set of reconstruction poles
for a plate.
"""
#Stick this in when fully converted to numerical plate codes
#plate=backwardsrots.MovingPlate[0]
npole=rotkit.make_points([90.],[0.],[0],[0],[0])
vgps=pd.concat([npole, rotkit.rotate_points(npole,backwardsrots)], ignore_index=True)
return vgps
def reconstruct_DI(backwardsrots,point):
"""
Given a set of reconstruction poles and a point in modern day
co-ordinates, calculates the expected D and I of ChRM formed at each pole
age in present day, without any tectonic effects.
2 tables returned: rotation points with predicted D,I appended, VGPS
"""
forwardrots=rotkit.invert_covrots(backwardsrots)
pastpoints=pd.concat([point,rotkit.rotate_points(point,backwardsrots)], ignore_index=True)
vgps=get_VGPs(forwardrots)
paleoI=np.arctan(2*np.tan(pastpoints.PointLat*np.pi/180))*180/np.pi
pp=np.sin((90-vgps.PointLat)*np.pi/180)
sitelong=point.iloc[0]['PointLong']
dphi=np.sin((vgps.PointLong-sitelong)*np.pi/180)
pm=np.sin((90-pastpoints.PointLat)*np.pi/180)
paleoD=np.arcsin(pp*dphi/pm)*180/np.pi
predDI=pd.DataFrame(np.column_stack((paleoD,paleoI)), columns=['PredDec','PredInc'])
return pd.concat([pastpoints, predDI],axis=1), vgps
def get_R(dir):
#given a direction with a kappa and N, calculate R
R=dir.n-(dir.n-1)/dir.k
return R
def get_fish(dir):
"""
generate fisher distributed points according to the supplied parameters
(includes dec,inc,n,k) in a pandas object.
"""
tempD,tempI=[],[]
for k in range(int(dir.n)):
dec,inc=pmag.fshdev(dir.k)
drot,irot=pmag.dodirot(dec,inc,dir.dec,dir.inc)
tempD.append(drot)
tempI.append(irot)
return np.column_stack((tempD,tempI))
def get_bounds(DIs):
#2sigma bounds
bounds=[]
# convert to cartesian coordinates
cart=pmag.dir2cart(DIs).transpose()
min=int(0.025*len(cart[0]))
max=int(0.975*len(cart[0]))
for i in range(3):
comp=cart[i]
comp.sort()
bounds.append([comp[min],comp[max]])
return bounds
def common_dir_boot(dir1,dir2,bootsteps=1000,plot='no'):
#test for a common direction using bootstrap method
#directions given as pandas Series listing mean direction/pole parameters
#this is assuming published means without input directions, so bootstrap is a little different from the one
#described in Tauxe: not a pseudoselection from specified points but drawing from fisher distribution directly.
#adds a level of abstraction which hopefully does not invalidate procedure
BDI1,BDI2=[],[]
for s in range(bootsteps):
fpars1=pmag.fisher_mean(get_fish(dir1))
fpars2=pmag.fisher_mean(get_fish(dir2))
BDI1.append([fpars1['dec'],fpars1['inc']])
BDI2.append([fpars2['dec'],fpars2['inc']])
bounds1=get_bounds(BDI1)
bounds2=get_bounds(BDI2)
#now want to check if is a pass or a fail - only pass if error bounds overlap in x,y,and z
bresult=[]
for b1,b2 in zip(bounds1,bounds2):
if (b1[0]>b2[1] or b1[1]<b2[0]):
bresult.append(0)
else: bresult.append(1)
if sum(bresult)==3: outcome='Pass'
else: outcome='Fail'
angle=pmag.angle((dir1['dec'],dir1['inc']),(dir2['dec'],dir2['inc']))
# set up plots - can run this if want to visually check what's going on.
if plot=='yes':
CDF={'X':1,'Y':2,'Z':3}
pmagplotlib.plot_init(CDF['X'],4,4)
pmagplotlib.plot_init(CDF['Y'],4,4)
pmagplotlib.plot_init(CDF['Z'],4,4)
# draw the cdfs
pmagplotlib.plotCOM(CDF,BDI1,BDI2,["",""])
pmagplotlib.drawFIGS(CDF)
return pd.Series([outcome,angle[0]],index=['Outcome','angle'])
def common_dir_MM90(dir1,dir2,NumSims=5000,plot='no'):
dir1['r']=get_R(dir1)
dir2['r']=get_R(dir2)
#largely based on iWatsonV routine of Swanson-Hyell in IPMag
cart_1=pmag.dir2cart([dir1["dec"],dir1["inc"],dir1["r"]])
cart_2=pmag.dir2cart([dir2['dec'],dir2['inc'],dir2["r"]])
Sw=dir1['k']*dir1['r']+dir2['k']*dir2['r'] # k1*r1+k2*r2
xhat_1=dir1['k']*cart_1[0]+dir2['k']*cart_2[0] # k1*x1+k2*x2
xhat_2=dir1['k']*cart_1[1]+dir2['k']*cart_2[1] # k1*y1+k2*y2
xhat_3=dir1['k']*cart_1[2]+dir2['k']*cart_2[2] # k1*z1+k2*z2
Rw=np.sqrt(xhat_1**2+xhat_2**2+xhat_3**2)
V=2*(Sw-Rw)
# keep weighted sum for later when determining the "critical angle"
# let's save it as Sr (notation of McFadden and McElhinny, 1990)
Sr=Sw
# do monte carlo simulation of datasets with same kappas as data,
# but a common mean
counter=0
Vp=[] # set of Vs from simulations
for k in range(NumSims):
# get a set of N1 fisher distributed vectors with k1,
# calculate fisher stats
Dirp=[]
for i in range(int(dir1["n"])):
Dirp.append(pmag.fshdev(dir1["k"]))
pars_p1=pmag.fisher_mean(Dirp)
# get a set of N2 fisher distributed vectors with k2,
# calculate fisher stats
Dirp=[]
for i in range(int(dir2["n"])):
Dirp.append(pmag.fshdev(dir2["k"]))
pars_p2=pmag.fisher_mean(Dirp)
# get the V for these
Vk=pmag.vfunc(pars_p1,pars_p2)
Vp.append(Vk)
# sort the Vs, get Vcrit (95th percentile one)
Vp.sort()
k=int(.95*NumSims)
Vcrit=Vp[k]
# equation 18 of McFadden and McElhinny, 1990 calculates the critical
# value of R (Rwc)
Rwc=Sr-(Vcrit/2)
# following equation 19 of McFadden and McElhinny (1990) the critical
# angle is calculated. If the observed angle (also calculated below)
# between the data set means exceeds the critical angle the hypothesis
# of a common mean direction may be rejected at the 95% confidence
# level. The critical angle is simply a different way to present
# Watson's V parameter so it makes sense to use the Watson V parameter
# in comparison with the critical value of V for considering the test
# results. What calculating the critical angle allows for is the
# classification of McFadden and McElhinny (1990) to be made
# for data sets that are consistent with sharing a common mean.
k1=dir1['k']
k2=dir2['k']
R1=dir1['r']
R2=dir2['r']
critical_angle=np.degrees(np.arccos(((Rwc**2)-((k1*R1)**2)
-((k2*R2)**2))/
(2*k1*R1*k2*R2)))
D1=(dir1['dec'],dir1['inc'])
D2=(dir2['dec'],dir2['inc'])
angle=pmag.angle(D1,D2)
if V<Vcrit:
outcome='Pass'
if critical_angle<5: MM90class='A'
elif critical_angle<10: MM90class='B'
elif critical_angle<20: MM90class='C'
else: MM90class='INDETERMINATE'
else:
outcome='Fail'
MM90class='FAIL'
result=pd.Series([outcome,V,Vcrit,angle[0],critical_angle,MM90class], index=['Outcome','VWatson','Vcrit','angle','critangle','MM90class'])
if plot=='yes':
CDF={'cdf':1}
#pmagplotlib.plot_init(CDF['cdf'],5,5)
p1 = pmagplotlib.plotCDF(CDF['cdf'],Vp,"Watson's V",'r',"")
p2 = pmagplotlib.plotVs(CDF['cdf'],[V],'g','-')
p3 = pmagplotlib.plotVs(CDF['cdf'],[Vp[k]],'b','--')
pmagplotlib.drawFIGS(CDF)
return result
#this is slightly modified so that only the points and ellipses are plotted, and makes it a subfigure rather than a separate one.
def plot_APWP(APWPlats,APWPlongs,A95,colour='blue',meridian=-100):
#generates a polar plot of the smoothed APWP when sent the columns from the pandas dataframe generated by generate_APWP
#pandas Dataframe columns not readable by Basemap initially...
longs=np.array(APWPlongs, dtype='float')
lats=np.array(APWPlats, dtype='float')
errors=np.array(A95, dtype='float')
m = Basemap(projection='nplaea',boundinglat=60,lon_0=meridian,resolution='l')
m.drawcoastlines(linewidth=0.25)
m.fillcontinents(color='lightgrey')
parallels = np.arange(-90.,90.,30.)
m.drawparallels(parallels)
meridians = np.arange(0.,360.,30.)
m.drawmeridians(meridians)
plt_line(longs,lats,m,colour)
plt_points(longs,lats,m,colour)
for i in range(len(errors)):
m.ellipse(longs[i],lats[i],errors[i],errors[i],0.)
#ell_x,ell_y=make_ellipse(lats[i],longs[i], errors[i])
#plt_line(ell_x,ell_y,m,colour)
return m
def plt_line(longs,lats,m,colour='black'):
x,y= m(longs,lats)
#note: zorder is a very useful thing. Higher number=higher plot level.
m.plot(x,y,color=colour, zorder=3)
def plt_points(longs,lats,m,colour='black'):
x,y= m(longs,lats)
#note: zorder is a very useful thing. Higher number=higher plot level.
m.scatter(x,y,10,color=colour, zorder=4)
def make_ellipse(centrelat,centrelong,A95):
#simplified adaptation of pmagplotlib.plotELL for plotting APWP A95 - note potential for using beta and gamma as dp/dm
degrad=np.pi/180
Pinc,Pdec=centrelat,centrelong
isign=abs(Pinc)/Pinc
beta=A95
Bdec=Pdec
Binc=Pinc-isign*90
gamma=A95
Gdec=Pdec+90
Ginc=0.
if beta > 90. or gamma>90:
beta=180.-beta
gamma=180.-beta
Pdec=Pdec-180.
Pinc=-Pinc
beta,gamma=beta*degrad,gamma*degrad # convert to radians
X_ell,Y_ell=[],[]
nums=201
xnum=float(nums-1.)/2.
# set up t matrix
t=[[0,0,0],[0,0,0],[0,0,0]]
X=dir2cart((Pdec,Pinc,1.0))
# set up rotation matrix t
t[0][2]=X[0]
t[1][2]=X[1]
t[2][2]=X[2]
X=dir2cart((Bdec,Binc,1.0))
t[0][0]=X[0]
t[1][0]=X[1]
t[2][0]=X[2]
X=dir2cart((Gdec,Ginc,1.0))
t[0][1]=X[0]
t[1][1]=X[1]
t[2][1]=X[2]
# set up v matrix
v=[0,0,0]
for i in range(nums): # incremental point along ellipse
psi=float(i)*np.pi/xnum
v[0]=np.sin(beta)*np.cos(psi)
v[1]=np.sin(gamma)*np.sin(psi)
v[2]=np.sqrt(1.-v[0]**2 - v[1]**2)
elli=[0,0,0]
# calculate points on the ellipse
for j in range(3):
for k in range(3):
elli[j]=elli[j] + t[j][k]*v[k] # cartesian coordinate j of ellipse
PTS=(cart2dir(elli))
X_ell.append(PTS[0])
Y_ell.append(PTS[1])
return X_ell,Y_ell
def dir2cart(d):
# converts list or array of vector directions, in degrees, to array of cartesian coordinates, in x,y,z
ints=np.ones(len(d)).transpose() # get an array of ones to plug into dec,inc pairs
d=np.array(d)
rad=np.pi/180.
if len(d.shape)>1: # array of vectors
decs,incs=d[:,0]*rad,d[:,1]*rad
if d.shape[1]==3: ints=d[:,2] # take the given lengths
else: # single vector
decs,incs=np.array(d[0])*rad,np.array(d[1])*rad
if len(d)==3:
ints=np.array(d[2])
else:
ints=np.array([1.])
cart= np.array([ints*np.cos(decs)*np.cos(incs),ints*np.sin(decs)*np.cos(incs),ints*np.sin(incs)]).transpose()
return cart
def cart2dir(cart):
"""
converts a direction to cartesian coordinates
"""
cart=np.array(cart)
rad=np.pi/180. # constant to convert degrees to radians
if len(cart.shape)>1:
Xs,Ys,Zs=cart[:,0],cart[:,1],cart[:,2]
else: #single vector
Xs,Ys,Zs=cart[0],cart[1],cart[2]
Rs=np.sqrt(Xs**2+Ys**2+Zs**2) # calculate resultant vector length
Decs=(np.arctan2(Ys,Xs)/rad)%360. # calculate declination taking care of correct quadrants (arctan2) and making modulo 360.
try:
Incs=np.arcsin(Zs/Rs)/rad # calculate inclination (converting to degrees) #
except:
print 'trouble in cart2dir' # most likely division by zero somewhere
return np.zeros(3)
return np.array([Decs,Incs,Rs]).transpose() # return the directions list
def smooth_APWP(file):
data=pd.read_table(file, header=0, sep=' ')
#these are the age points that will eventually be interpolated to - making sure it's a nice smooth curve
xi=np.linspace(data.Age[0],data.Age[len(data.Age)-1],100)
#fit spline to Longitude data; the weighting function w is set to biases towards low A95 using a Guassian distribution, sd 15.
tck=splrep(data.Age,data.PoleLong, k=3,w=np.exp(-(data.A95**2/450)))
#produce interpolated Longitude values
intLong = splev(xi, tck)
#same procedure for Latitude
tck=splrep(data.Age,data.PoleLat, k=3,w=np.exp(-(data.A95**2/450)))
intLat = splev(xi, tck)
fig=plt.figure(num=None, figsize=(10, 5), dpi=150, facecolor='w', edgecolor='k')
fig.suptitle(file, fontsize=12)
plt.subplot(1, 2, 1)
#x y plot showing Latitude/Longitude and smoothed curve
plt.plot(data.PoleLat, data.PoleLong, '.', intLat, intLong)
plt.subplot(1, 2, 2)
#polar plot showing smoothed APWP
m=plot_APWP(data.PoleLat,data.PoleLong,data.A95)
plt_line(intLong,intLat,m,'red')