shipgrav 1.0.0__py2.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.
shipgrav/grav.py ADDED
@@ -0,0 +1,1260 @@
1
+ import numpy as np
2
+ from scipy.signal import lfilter, firwin
3
+ from pandas import DataFrame
4
+ from statsmodels.api import OLS, add_constant
5
+ from datetime import datetime, timezone
6
+ from math import factorial
7
+ from scipy.special import erf, erfcinv
8
+ from copy import copy
9
+
10
+ # impulse response of 10th order Taylor series differentiator
11
+ tay10 = [1/1260, -5/504, 5/84, -5/21, 5/6,
12
+ 0, -5/6, 5/21, -5/84, 5/504, -1/1260]
13
+
14
+ ########################################################################
15
+ # tidal correction
16
+ ########################################################################
17
+
18
+
19
+ def _convert_datetime_tidetime(timestamp):
20
+ """Calculate julian century and hour from a timestamp.
21
+
22
+ The reference point is noon on December 31, 1899 per Longman's paper.
23
+
24
+ :param timestamp: time to convert to century + hour
25
+ :type timestamp: datetime.datetime
26
+
27
+ :returns:
28
+ - **julian century** (*int*) - centuries since reference date, days/36525
29
+ - **julian_hour** (*float*) - hours since midnight
30
+ """
31
+ origin_time = datetime(1899, 12, 31, 12, 0, 0, tzinfo=timezone.utc)
32
+ dt = timestamp - origin_time
33
+ days = dt.days + dt.seconds/3600./24.
34
+ julian_century = days/36525
35
+ julian_hour = (timestamp.hour + timestamp.minute /
36
+ 60. + timestamp.second/3600.)
37
+
38
+ return julian_century, julian_hour
39
+
40
+
41
+ def longman_tide_prediction(lon, lat, times, alt=0, return_components=False):
42
+ """ Calculate predicted tidal effect on gravity using the Longman algorithm.
43
+
44
+ The calculation is taken directly from
45
+
46
+ Longman (1959). Formulas for Computing the Tidal Accelerations Due to
47
+ the Moon and the Sun. Journal of Geophysical Research 64(12),
48
+ DOI: 10.1029/JZ064i012p02351
49
+
50
+ as are all of the constant and variable descriptions.
51
+ Tidal contribution(s) returned are in mGal.
52
+
53
+ :param lon: longitude in decimal degrees, positive E
54
+ :type lon: array_like
55
+ :param lat: latitude in decimal degrees, positive N
56
+ :type lat: array_like
57
+ :param times: times for geographic locations
58
+ :type times: array_like, datetime.datetime
59
+ :param alt: altitude in meters (0 for sea level, for marine grav)
60
+ :type alt: array_like
61
+ :param return_components: if True, return lunar and solar components with total.
62
+ :type return_components: bool
63
+
64
+ :returns:
65
+ - **g0** (*ndarray*)- total tidal effect in mGal
66
+ - **gm** (*ndarray*)- lunar tidal effect in mGal (optional, if return_components is True)
67
+ - **gs** (*ndarray*)- solar tidal effect in mGal (optional, if return_components is True)
68
+ """
69
+
70
+ assert len(lon) == len(lat), 'lengths of input vectors must be the same'
71
+ assert len(lon) == len(times), 'lengths of input vectors must be the same'
72
+
73
+ # convert the timestamps, referenced to Longman origin time
74
+ T, t0 = np.empty(len(times)), np.empty(len(times))
75
+ for i, stamp in enumerate(times):
76
+ a, b = _convert_datetime_tidetime(stamp)
77
+ T[i] = a
78
+ t0[i] = b
79
+ if t0[i] < 0:
80
+ t0[i] += 24
81
+ if t0[i] >= 24:
82
+ t0[i] == 24
83
+
84
+ TT = T*T # squares and cubes are used a lot so shortcut them
85
+ TTT = T*T*T
86
+
87
+ # define a bunch of constants for the calculation
88
+ mu = 6.673e-8 # Newton's gravitational constant
89
+ M = 7.3537e25 # Mass of the moon in grams
90
+ S = 1.993e33 # Mass of the sun in grams
91
+ e = 0.05490 # Eccentricity of the moon's orbit
92
+ m = 0.074804 # Ratio of mean motion of the sun to that of the moon
93
+ c = 3.84402e10 # Mean distance between the centers of the earth and the moon
94
+ c1 = 1.495e13 # Mean distance between centers of the earth and sun in cm
95
+ h2 = 0.612 # Love parameter
96
+ k2 = 0.303 # Love parameter
97
+ a = 6.378270e8 # Earth's equitorial radius in cm
98
+ i = 0.08979719 # (i) Inclination of the moon's orbit to the ecliptic
99
+ # Inclination of the Earth's equator to the ecliptic 23.452 degrees
100
+ omega = np.radians(23.452)
101
+ lamb = np.radians(lat) # (lambda) Latitude of point
102
+ H = alt * 100. # (H) Altitude above sea-level of point P in cm
103
+
104
+ # Longman convention has W+/E- for unknown reasons
105
+ L = -lon
106
+
107
+ # Lunar gravity effects (Shureman 1941 coeffs)
108
+ # mean longitude of the moon in its orbit, reckoned from the referred equinox
109
+ s = 4.720023434 + 8399.709299*T + 0.0000440696*TT # skip 3rd order bc it is tiny
110
+ # mean longitude of lunar perigee
111
+ p = 5.835124713 + 71.01800936*T - 0.000180545*TT - 0.00000021817*TTT
112
+ # mean longitude of the sun
113
+ h = 4.88162792 + 628.3319509*T + 0.00000527962*TT
114
+ # longitude of moon's ascending node in orbit reckoned from referred equinox
115
+ N = 4.523588564 - 33.75715303*T + 0.000036749*TT # skip 3rd order bc it is tiny
116
+
117
+ # inclination of the moon's orbit to the equator
118
+ I = np.arccos(np.cos(omega)*np.cos(i) - np.sin(omega)*np.sin(i)*np.cos(N))
119
+ # longitude in the celestial equator of its intersection A with the moon's orbit
120
+ nu = np.arcsin(np.sin(i)*np.sin(N)/np.sin(I))
121
+ # hour angle of mean sun measured westward from the place of observations
122
+ t = np.radians(15. * (t0 - 12) - L)
123
+
124
+ # right ascension of meridian of place of observations reckoned from A
125
+ chi = t + h - nu
126
+ # cos(alpha) where alpha is defined in eq. 15 and 16 of Longman 1959
127
+ cos_alpha = np.cos(N)*np.cos(nu) + np.sin(N)*np.sin(nu)*np.cos(omega)
128
+ # sin(alpha) where alpha is defined in eq. 15 and 16 of Longman 1959
129
+ sin_alpha = np.sin(omega)*np.sin(N)/np.sin(I)
130
+ # alpha from eq. 18 of Longman 1959
131
+ alpha = 2*np.arctan(sin_alpha/(1 + cos_alpha))
132
+ # longitude in the moon's orbit of its ascending intersection with the celestial equator
133
+ xi = N - alpha
134
+
135
+ # mean longitude of moon in radians in its orbit reckoned from A
136
+ sigma = s - xi
137
+ # longitude of moon in its orbit reckoned from its ascending intersection with the equator
138
+ L_moon = (sigma + 2*e*np.sin(s - p) + (5./4)*e*e*np.sin(2*(s - p)) +
139
+ (15./4)*m*e*np.sin(s - 2*h + p) + (11./8)*m*m*np.sin(2*(s - h)))
140
+
141
+ # Solar calculations
142
+ # mean longitude of solar perigee
143
+ p1 = 4.908229461 + 0.03000526416*T + 0.000007902463*TT # skip tiny 3rd order term
144
+ # eccentricity of Earth's orbit
145
+ e1 = 0.01675104 - 0.0000418*T - 0.000000126*TT
146
+
147
+ # right ascension of meridian of place of observations reckoned from the vernal equinox
148
+ chi1 = t + h
149
+ # longitude of sun in the ecliptic reckoned from the vernal equinox
150
+ L_sun = h + 2*e1*np.sin(h - p1)
151
+ # cosine(theta) where theta is the zenith angle of the moon
152
+ cos_theta = (np.sin(lamb)*np.sin(I)*np.sin(L_moon) + np.cos(lamb)*(np.cos(0.5*I)**2
153
+ * np.cos(L_moon - chi) + np.sin(0.5*I)**2*np.cos(L_moon + chi)))
154
+ # cosine(phi) where phi is the zenith angle of the sun
155
+ cos_phi = (np.sin(lamb)*np.sin(omega)*np.sin(L_sun) + np.cos(lamb) *
156
+ (np.cos(0.5*omega)**2*np.cos(L_sun - chi1) +
157
+ np.sin(0.5*omega)**2*np.cos(L_sun + chi1)))
158
+
159
+ # Distance
160
+ # distance parameter, eq. 34 in Longman 1959
161
+ C = np.sqrt(1./(1 + 0.006738*np.sin(lamb)**2))
162
+ # distance from point P to the center of the Earth
163
+ r = C*a + H
164
+ # distance parameter, eq. 31 in Longman 1959
165
+ aprime = 1. / (c * (1 - e * e))
166
+ # distance parameter, eq. 32 in Longman 1959
167
+ aprime1 = 1. / (c1 * (1 - e1 * e1))
168
+ # distance between centers of the Earth and the moon
169
+ d = (1./((1./c) + aprime*e*np.cos(s - p) + aprime*e*e *
170
+ np.cos(2*(s - p)) + (15./8)*aprime*m*e*np.cos(s - 2*h + p)
171
+ + aprime*m*m*np.cos(2*(s - h))))
172
+ # distance between centers of the Earth and the sun
173
+ D = 1./((1./c1) + aprime1*e1*np.cos(h - p1))
174
+
175
+ # vertical component of tidal acceleration due to the moon
176
+ gm = ((mu*M*r/(d*d*d))*(3*cos_theta**2 - 1) + (3./2) *
177
+ (mu*M*r*r/(d*d*d*d))*(5*cos_theta**3 - 3*cos_theta))
178
+ # vertical component of tidal acceleration due to the sun
179
+ gs = mu*S*r/(D*D*D)*(3*cos_phi**2 - 1)
180
+
181
+ love = (1 + h2 - 1.5*k2)
182
+ g0 = (gm + gs) * 1e3*love
183
+ if return_components:
184
+ return g0, gm*1e3*love, gs*1e3*love
185
+ else:
186
+ return g0
187
+
188
+ ########################################################################
189
+ # Eotvos correction
190
+ ########################################################################
191
+
192
+
193
+ def eotvos_full(lon, lat, ht, samp, a=6378137.0, b=6356752.3142):
194
+ """ Full Eotvos correction in mGals.
195
+
196
+ The Eotvos correction is the effect on measured gravity due to horizontal
197
+ motion over the Earth's surface.
198
+
199
+ This formulation is from Harlan (1968), "Eotvos Corrections for Airborne Gravimetry" in
200
+ *Journal of Geophysical Research*, 73(14), DOI: 10.1029/JB073i014p04675
201
+
202
+ Implementation modified from matlab script written by Sandra Preaux, NGS, NOAA August 24 2009
203
+
204
+ Components of the correction:
205
+
206
+ * rdoubledot
207
+ * angular acceleration of the reference frame
208
+ * corliolis
209
+ * centrifugal
210
+ * centrifugal acceleration of Earth
211
+
212
+ :param lon: longitudes in degrees
213
+ :type lon: array_like
214
+ :param lat: latitudes in degrees
215
+ :type lat: array_like
216
+ :param ht: elevation (referenced to sea level)
217
+ :type ht: array_like
218
+ :param samp: sampling rate
219
+ :type samp: float
220
+ :param a: major axis of ellipsoid (default is WGS84)
221
+ :type a: float, optional
222
+ :param b: minor axis of ellipsoid (default is WGS84)
223
+ :type b: float, optional
224
+
225
+ :return: **E** (*ndarray*), Eotvos correction in mGal
226
+ """
227
+ We = 0.00007292115 # siderial rotation rate, radians/sec
228
+ mps2mgal = 100000 # m/s/s to mgal
229
+ ecc = (a-b)/a
230
+
231
+ latr = np.deg2rad(lat)
232
+ lonr = np.deg2rad(lon)
233
+
234
+ # get time derivatives of position
235
+ dlat = center_diff(latr, 1, samp)
236
+ ddlat = center_diff(latr, 2, samp)
237
+ dlon = center_diff(lonr, 1, samp)
238
+ ddlon = center_diff(lonr, 2, samp)
239
+ dht = center_diff(ht, 1, samp)
240
+ ddht = center_diff(ht, 2, samp)
241
+
242
+ # sines and cosines etc
243
+ slat = np.sin(latr[1:-1])
244
+ clat = np.cos(latr[1:-1])
245
+ s2lat = np.sin(2*latr[1:-1])
246
+ c2lat = np.cos(2*latr[1:-1])
247
+
248
+ # calculate r' and its derivatives
249
+ rp = a*(1 - ecc*slat*slat)
250
+ drp = -a*dlat*ecc*s2lat
251
+ ddrp = -a*ddlat*ecc*s2lat - 2*a*dlat*dlat*ecc*c2lat
252
+
253
+ # calculate deviation from normal and derivatives
254
+ D = np.arctan(ecc*s2lat)
255
+ dD = 2*dlat*ecc*c2lat
256
+ ddD = 2*ddlat*ecc*c2lat - 4*dlat*dlat*ecc*s2lat
257
+
258
+ # define r and its derivatives
259
+ r = np.vstack((-rp*np.sin(D), np.zeros(len(rp)), -
260
+ rp*np.cos(D) - ht[1:-1])).T
261
+ rdot = np.vstack((-drp*np.sin(D) - rp*dD*np.cos(D),
262
+ np.zeros(len(rp)), -drp*np.cos(D) + rp*dD*np.sin(D) - dht)).T
263
+ ci = -ddrp*np.sin(D) - 2.*drp*dD*np.cos(D) - rp * \
264
+ (ddD*np.cos(D) - dD*dD*np.sin(D))
265
+ ck = -ddrp*np.cos(D) + 2.*drp*dD*np.sin(D) + rp * \
266
+ (ddD*np.sin(D) + dD*dD*np.cos(D) - ddht)
267
+ rdotdot = np.vstack((ci, np.zeros(len(ci)), ck)).T
268
+
269
+ # define w and derivative
270
+ w = np.vstack(((dlon + We)*clat, -dlat, -(dlon + We)*slat)).T
271
+ wdot = np.vstack((dlon*clat - (dlon + We)*dlat*slat, -
272
+ ddlat, -ddlon*slat - (dlon + We)*dlat*clat)).T
273
+
274
+ w2xrdot = np.cross(2*w, rdot)
275
+ wdotxr = np.cross(wdot, r)
276
+ wxr = np.cross(w, r)
277
+ wxwxr = np.cross(w, wxr)
278
+
279
+ # calcualte wexwexre, centrifugal acceleration due to the Earth
280
+ re = np.vstack((-rp*np.sin(D), np.zeros(len(rp)), -rp*np.cos(D))).T
281
+ we = np.vstack((We*clat, np.zeros(len(slat)), -We*slat)).T
282
+ wexre = np.cross(we, re)
283
+ wexwexre = np.cross(we, wexre)
284
+ wexr = np.cross(we, r)
285
+ wexwexr = np.cross(we, wexr)
286
+
287
+ # calculate total acceleration for the aircraft
288
+ a = rdotdot + w2xrdot + wdotxr + wxwxr
289
+
290
+ # Eotvos correction is the vertical component of the total acceleraton of
291
+ # the aircraft minus the centrifugal acceleration of the Earth, convert to mGal
292
+ E = (a[:, 2] - wexwexr[:, 2])*mps2mgal
293
+ E = np.hstack((E[0], E, E[-1]))
294
+
295
+ return E
296
+
297
+ ########################################################################
298
+ # latitude correction functions
299
+ ########################################################################
300
+
301
+
302
+ def free_air_second_order(lat, ht):
303
+ """ 2nd order free-air correction
304
+
305
+ :param lat: latitude, degrees
306
+ :type lat: array_like
307
+ :param height: elevation, meters
308
+ :type height: array_like
309
+
310
+ :return: free-air correction, mGal
311
+ """
312
+ s2lat = np.sin(np.deg2rad(lat))**2
313
+
314
+ return -((0.3087691 - 0.0004398*s2lat)*ht) + 7.2125e-8*(ht**2)
315
+
316
+
317
+ def wgs_grav(lat):
318
+ """ Theoretical gravity for WGS84 ellipsoid
319
+
320
+ :param lat: latitude, degrees
321
+ :type lat: array_like
322
+
323
+ :return: uniform ellipsoid gravity, mGal
324
+ """
325
+ sinsq = np.sin(np.deg2rad(lat))**2
326
+
327
+ num = 1 + 0.00193185265241*sinsq # something like (b*gp - a*ge)/(a*ge)
328
+ den = np.sqrt(1 - 0.00669437999014*sinsq) # this is e2
329
+ return 978032.53359*(num/den)
330
+
331
+ ########################################################################
332
+ # cross-coupling coefficients
333
+ ########################################################################
334
+
335
+
336
+ def calc_cross_coupling_coefficients(faa_in, vcc_in, ve_in, al_in, ax_in, level_in, times=None, samplerate=1):
337
+ """ Calculate cross-coupling coefficients from FAA via ordinary linear regression
338
+
339
+ The cross-coupling coefficients are returned in `model`, in the `params` attribute
340
+
341
+ :param faa_in: free air anomaly, filtered
342
+ :type faa_in: array_like
343
+ :param vcc_in: vcc monitor
344
+ :type vcc_in: array_like
345
+ :param ve_in: ve monitor
346
+ :type ve_in: array_like
347
+ :param al_in: al monitor
348
+ :type al_in: array_like
349
+ :param ax_in: ax monitor
350
+ :type ax_in: array_like
351
+ :param level_in: tilt/leveling correction, often negligible.
352
+ Use a vector of zeros to ignore this component.
353
+ :type level_in: array_like
354
+ :param times: timestamps to use for dividing the data into continuous sections
355
+ :type times: array_like, floats, optional
356
+ :param samplerate: used with times to detect large sampling gaps in the data
357
+ :type samplerate: float, optional
358
+
359
+ :return:
360
+ - **df** (*pd.DataFrame*)- double-differenced and filtered monitors and gravity
361
+ - **model** (*statsmodels.OLS*)- linear regression model
362
+ """
363
+
364
+ end_inds = np.array([len(faa_in),]) # just the one
365
+ if times is not None: # supplied a vector of timestamps to go with everything else
366
+ # split data into segments of continuous even sample rate
367
+ tdiff = np.diff(times)
368
+ if np.any(tdiff > 5*samplerate):
369
+ # 5 sec gap doesn't matter much I hope???
370
+ end_inds = np.where(tdiff > 5*samplerate)[0]
371
+
372
+ end_inds = np.append(-1, end_inds) # add starting point
373
+
374
+ faa_tf = np.array([])
375
+ vcc_tf = np.array([])
376
+ ve_tf = np.array([])
377
+ al_tf = np.array([])
378
+ ax_tf = np.array([])
379
+ le_tf = np.array([]) # things to fit
380
+
381
+ for i in range(1, len(end_inds)): # loop continuous-time segments
382
+ faa = faa_in[end_inds[i-1]+1:end_inds[i]]
383
+ vcc = vcc_in[end_inds[i-1]+1:end_inds[i]]
384
+ ve = ve_in[end_inds[i-1]+1:end_inds[i]]
385
+ al = al_in[end_inds[i-1]+1:end_inds[i]]
386
+ ax = ax_in[end_inds[i-1]+1:end_inds[i]]
387
+ level = level_in[end_inds[i-1]+1:end_inds[i]]
388
+ if len(faa) < 1000:
389
+ continue # no point for very short segments
390
+ # double derivatives with taylor series
391
+ gpp = np.convolve(np.convolve(faa, tay10, 'same'), tay10, 'same')
392
+ vccpp = np.convolve(np.convolve(vcc, tay10, 'same'), tay10, 'same')
393
+ vepp = np.convolve(np.convolve(ve, tay10, 'same'), tay10, 'same')
394
+ alpp = np.convolve(np.convolve(al, tay10, 'same'), tay10, 'same')
395
+ axpp = np.convolve(np.convolve(ax, tay10, 'same'), tay10, 'same')
396
+ levpp = np.convolve(np.convolve(level, tay10, 'same'), tay10, 'same')
397
+
398
+ # trim the ends
399
+ gpp = gpp[10:-10]
400
+ vccpp = vccpp[10:-10]
401
+ vepp = vepp[10:-10]
402
+ alpp = alpp[10:-10]
403
+ axpp = axpp[10:-10]
404
+ levpp = levpp[10:-10]
405
+
406
+ # fairly narrow filter to get rid of any high-freq noise generated by the double derivation
407
+ filterlength = 100 # Aliod code has this as 10...but that gives VERY different cc values
408
+ taps = int(2*filterlength)
409
+ BM = firwin(taps, 1/taps, window='blackman')
410
+ fgpp = lfilter(BM, 1, gpp)
411
+ fvccpp = lfilter(BM, 1, vccpp)
412
+ fvepp = lfilter(BM, 1, vepp)
413
+ falpp = lfilter(BM, 1, alpp)
414
+ faxpp = lfilter(BM, 1, axpp)
415
+ flevpp = lfilter(BM, 1, levpp)
416
+
417
+ # trim off filter transients
418
+ fgpp = fgpp[taps:-taps]
419
+ fvccpp = fvccpp[taps:-taps]
420
+ fvepp = fvepp[taps:-taps]
421
+ falpp = falpp[taps:-taps]
422
+ faxpp = faxpp[taps:-taps]
423
+ flevpp = flevpp[taps:-taps]
424
+
425
+ faa_tf = np.append(faa_tf, fgpp)
426
+ vcc_tf = np.append(vcc_tf, fvccpp)
427
+ ve_tf = np.append(ve_tf, fvepp)
428
+ al_tf = np.append(al_tf, falpp)
429
+ ax_tf = np.append(ax_tf, faxpp)
430
+ le_tf = np.append(le_tf, flevpp)
431
+
432
+ faa_tf = np.array(faa_tf).flatten()
433
+ vcc_tf = np.array(vcc_tf).flatten()
434
+ ve_tf = np.array(ve_tf).flatten()
435
+ al_tf = np.array(al_tf).flatten()
436
+ ax_tf = np.array(ax_tf).flatten()
437
+ le_tf = np.array(le_tf).flatten()
438
+
439
+ # solve for curvature equation by simple regression (OLS)
440
+ df = DataFrame({'ve': ve_tf, 'vcc': vcc_tf, 'al': al_tf,
441
+ 'ax': ax_tf, 'lev': le_tf, 'g': -faa_tf})
442
+ x = df[['ve', 'vcc', 'al', 'ax', 'lev']]
443
+ y = df['g']
444
+ x = add_constant(x)
445
+ model = OLS(y, x).fit()
446
+ return df, model
447
+
448
+ ########################################################################
449
+ # things loosely connected to tilt corrections
450
+ ########################################################################
451
+
452
+
453
+ def center_diff(y, n, samp):
454
+ """ Numerical derivatives, central difference of nth order
455
+
456
+ :param y: data to differentiate
457
+ :type y: array_like
458
+ :param n: order, either 1 or 2
459
+ :type n: int
460
+ :param samp: sampling rate
461
+ :type samp: float
462
+
463
+ :return: 1st or 2nd order derivative of **y**
464
+ """
465
+ if n == 1:
466
+ return (y[2:] - y[:-2])*(samp/2)
467
+ elif n == 2:
468
+ return (y[:-2] - 2*y[1:-1] + y[2:])*(samp**2)
469
+ else:
470
+ print('bad order for derivative')
471
+ return -999
472
+
473
+
474
+ def up_vecs(dt, g, cacc, lacc, on_off, cper, cdamp, lper, ldamp):
475
+ """ Calculate 3xN matrix of platform up-pointing vectors in (cross, long) coordinates
476
+
477
+ :param dt: sampling interval in seconds
478
+ :type dt: float
479
+ :param g: latitudinal correction term (2nd orfer FA plus ellipsoid)
480
+ :type g: array_like
481
+ :param cacc: cross-axis acceleration
482
+ :type cacc: array_like
483
+ :param lacc: long-axis acceleration
484
+ :type lacc: array_like
485
+ :param on_off: flag for "good" data - can be used to zero out data points when the meter
486
+ is clamped or otherwise not operational
487
+ :type on_off: array_like
488
+ :param cper: platform period in seconds for the cross-axis tilt filter
489
+ :type cper: float
490
+ :param cdamp: platform damping term for cross-axis tilt filter
491
+ :type cdamp: float
492
+ :param lper: platform period in seconds for the long-axis tilt filter
493
+ :type lper: float
494
+ :param ldamp: platform damping term for long-axis tilt filter
495
+ :type ldamp: float
496
+
497
+ :return: **up_vecs** (*3xN ndarray*) - cross, long, and up vectors for the platform
498
+
499
+ """
500
+ # clean out any nans in the accelerations
501
+ cacc[np.isnan(cacc)] = 0
502
+ lacc[np.isnan(lacc)] = 0
503
+
504
+ # apply the mysterious on_off
505
+ cacc[on_off > 0] = 0
506
+ lacc[on_off > 0] = 0
507
+
508
+ # make the cross-axis tilt filter
509
+ cnum, cden = _tilt_filter(cper, dt, cdamp)
510
+
511
+ # calculate cross-axis driving term, do tilt filtering
512
+ drive = cacc/g
513
+ drive[np.isnan(drive)] = 0
514
+ ctilt = lfilter(cnum, cden, drive)
515
+
516
+ # repeat all that for the long axis
517
+ lnum, lden = _tilt_filter(lper, dt, ldamp)
518
+ drive = lacc/g
519
+ drive[np.isnan(drive)] = 0
520
+ ltilt = lfilter(lnum, lden, drive)
521
+
522
+ # combine the pieces to get the platform up-pointing vectors
523
+ up_vecs = _calc_up_vecs(np.arctan(ctilt), np.arctan(ltilt))
524
+
525
+ return up_vecs
526
+
527
+
528
+ def _tilt_filter(per, dt, damp=False):
529
+ """ Filter coefficients for L&R platform tilt computation
530
+
531
+ :param per: platform period, seconds
532
+ :type per: float
533
+ :param dt: sample increment, seconds
534
+ :type dt: float
535
+ :param damp: platform damping term (default: sqrt(2)/2)
536
+ :type damp: float, optional
537
+
538
+ :return: filter coefficients
539
+ """
540
+
541
+ if not damp:
542
+ damp = np.sqrt(2)/2
543
+
544
+ # frequencies:
545
+ w0 = (2*np.pi)/per
546
+ ws = 1/np.sqrt(6371100/9.8)
547
+
548
+ om0 = (2/dt)*np.tan(w0*dt/2)
549
+ omS = (2/dt)*np.tan(ws*dt/2)
550
+
551
+ # first stage substitutions
552
+ a = (omS**2) - (om0**2)
553
+ b = 2*damp*om0*(2/dt)
554
+ c = 4/(dt**2)
555
+ d = om0**2
556
+
557
+ # second stage substitutions
558
+ d0 = b + c + d
559
+ b0 = (a - b)/d0
560
+ b1 = 2*a/d0
561
+ b2 = (a + b)/d0
562
+ a1 = 2*(d - c)/d0
563
+ a2 = (c + d - b)/d0
564
+
565
+ num = np.array([b0, b1, b2])
566
+ den = np.array([1, a1, a2])
567
+
568
+ return num, den
569
+
570
+
571
+ def _calc_up_vecs(ctilt, ltilt):
572
+ """ calculate 3xN matrix of platform up-vectors in (cross, long, up) coordinates
573
+
574
+ :param ctilt: cross-axis tilt angles in radians
575
+ :type ctilt: array_like
576
+ :param ltilt: long-axis tilt angles in radians
577
+ :type ltilt: array_like
578
+
579
+ :return: **up_vecs** (*3xN ndarray*) - (cross, long, up) for platform
580
+ """
581
+
582
+ # get increments, assuming initial is 0
583
+ inc_ct = np.append(ctilt[0], np.diff(ctilt))
584
+ inc_lt = np.append(ltilt[0], np.diff(ltilt))
585
+
586
+ # trig functions of tilts
587
+ sc = np.sin(ctilt)
588
+ cc = np.cos(ctilt)
589
+ sl = np.sin(ltilt)
590
+ cl = np.cos(ltilt)
591
+
592
+ # set up array to hold output
593
+ n = len(ctilt)
594
+ up_vecs = np.zeros((3, n))
595
+
596
+ # do rotations
597
+ for i in range(n):
598
+ # rotation matrics
599
+ rotc = np.array([[cc[i], 0, -sc[i]], [0, 1, 0], [sc[i], 0, cc[i]]])
600
+ rotl = np.array([[1, 0, 0], [0, cl[i], -sl[i]], [0, sl[i], cl[i]]])
601
+
602
+ # alternate order of rotations for reasons that I do not understand
603
+ plat_up = [0, 0, 1] # start with vertical platform
604
+ if i % 2 == 0:
605
+ plat_up = np.matmul(rotc, np.matmul(rotl, plat_up))
606
+ else:
607
+ plat_up = np.matmul(rotl, np.matmul(rotc, plat_up))
608
+
609
+ up_vecs[:, i] = plat_up
610
+
611
+ return up_vecs
612
+
613
+ ########################################################################
614
+ # MBA and RMBA functions (including thermal models)
615
+ ########################################################################
616
+
617
+
618
+ def grav1d_padded(xtopo, topo, zlev, rho):
619
+ """Calculate the gravity anomaly due to a density contrast across topography, along a line.
620
+
621
+ The input (1D) topography is padded on both ends to reduce edge effects
622
+
623
+ This function uses the method from Parker and Blakely:
624
+
625
+ R. L. Parker (1972). The Rapid Calculation of Potential Anomalies,
626
+ Geophys J R astr Soc 31, 447-455, DOI: 10.1111/j.1365-246X.1973.tb06513.x
627
+
628
+ R. J. Blakely (1995). "Ch. 11: Fourier-Domain Modeling" in **Potential Theory in Gravity
629
+ and Magnetic Applications**, Cambridge University Press, DOI: 10.1017/CBO9780511549816
630
+
631
+ .. Original 2D function written by Mark Behn, November 6, 2003
632
+ Translated to Python in 1D with padding by Hannah Mark, 6 October 2017
633
+
634
+ :param xtopo: x coordinates of the surface in meters (must be equally spaced)
635
+ :type xtopo: array_like
636
+ :param topo: z coordinates of the surface in meters
637
+ :type ztopo: array_like
638
+ :param zlev: vertical distance for upwards continuation in meters.
639
+ :type zlev: float
640
+ :param rho: density contrast across the topography in kg/m^3.
641
+ :type rho: float
642
+
643
+ :return: **anom** (*ndarray*) - gravity anomaly in mgal
644
+ """
645
+ G = 6.673*1e-8
646
+ grav = 2*np.pi*G*rho
647
+ baselev = np.mean(topo) # mean topography for baseline
648
+
649
+ # spacing of coordinates, must be constant
650
+ dx = (xtopo[-1]-xtopo[0])/(len(xtopo)-1)
651
+ nx = len(xtopo)
652
+
653
+ wing = np.ones(2*len(topo)) # *** extra padding
654
+
655
+ # extend or mirror the profile
656
+ padtopo = np.append(topo[0]*wing, np.append(topo, topo[-1]*wing))
657
+ # padtopo = np.append(baselev*wing,np.append(topo,baselev*wing))
658
+ # padtopo = np.append(topo,topo[::-1])
659
+ nxt = len(padtopo)
660
+ mfx = (nxt/2.) + 1
661
+
662
+ k = (2*np.pi)/(nxt*dx) # calculate wavenumbers
663
+ k2 = k**2
664
+ xi1 = np.arange(1, mfx+1)
665
+ xi = np.append(xi1, xi1[-2:0:-1])
666
+ xxk = (xi-1)*(xi-1)*k2
667
+ kwn1 = np.sqrt(xxk[:nxt])
668
+
669
+ Ftopo = np.fft.fft(padtopo) # Fourier transform the topography
670
+ Ftopo[0] = 0
671
+
672
+ npower = 5
673
+ SUMtopo = copy(Ftopo)
674
+ for ip in range(2, npower+1): # summation per eq. 11.41 in Blakely
675
+ Ftopo = np.fft.fft(padtopo**ip)
676
+ Ftopo = Ftopo*(kwn1**(ip-1))/factorial(ip)
677
+
678
+ SUMtopo = SUMtopo + Ftopo
679
+
680
+ data = SUMtopo*grav*np.exp(-(zlev-baselev)*kwn1) # upward continuation
681
+
682
+ data = np.fft.ifft(data) # inverse Fourier transform
683
+ anom = np.real(data[2*nx:3*nx])*100 # factor of 100 for mgal output
684
+
685
+ return anom
686
+
687
+
688
+ def grav2d_folding(X, Y, Z, dx, dy, drho=0.6, dz=6000, ifold=True, npower=5):
689
+ """
690
+ Parker [1972] method for calculating gravity from 2D topographic surface with a density contrast.
691
+
692
+ The `ifold` option enables folding the input topography grid in x and y to mitigate edge effects
693
+
694
+ .. Modified from parker.m by Mark Behn, which was in turn modified from
695
+ parker.f by Ban-Yuan Kuo and Jian Lin.
696
+
697
+ :param X: vector of N X coordinates
698
+ :type X: ndarray
699
+ :param Y: vector of M Y coordinates
700
+ :type Y: ndarray
701
+ :param Z: matrix of Z coordinates
702
+ :type Z: ndarray, NxM
703
+ :param dx: x grid spacing, for wavenumbers [km]
704
+ :type dx: float
705
+ :param dy: y grid spacing, for wavenumbers [km]
706
+ :type dy: float
707
+ :param drho: density contrast across surface. Ex: 1.7 for water to crust, 0.6 for crust to mantle
708
+ :type drho: float
709
+ :param dz: offset depth for layer interface, added to baselevel for upward continuation [m]
710
+ :type dz: float
711
+ :param ifold: switch for folding
712
+ :type ifold: bool
713
+ :param npower: power of Taylor series expansion (default: 5)
714
+ :type npower: int
715
+
716
+ :return: (*ndarray*) gravity anomaly in mgal
717
+ """
718
+ nx = len(X) # size of the grid
719
+ ny = len(Y)
720
+
721
+ zmax = max(Z.flatten()) # get min and max for scaling
722
+ zmin = min(Z.flatten())
723
+ if zmax < -1e10:
724
+ zmax = -1e10
725
+ if zmin > 1e10:
726
+ zmin = 1e10
727
+
728
+ slev = np.mean(Z.flatten())
729
+
730
+ Z = 100*(slev-Z)
731
+ slev = slev*100
732
+ dx = 100000*dx
733
+ dy = 100000*dy
734
+ dz = dz*100
735
+
736
+ # to fold or not to fold--
737
+ if ifold:
738
+ nxt = nx*2
739
+ nyt = ny*2
740
+ else:
741
+ nxt = nx
742
+ nyt = ny
743
+
744
+ G = 6.673e-8 # gravitational constant
745
+ conv = 1000 # gals to mgals
746
+ grav1 = 2*np.pi*G*conv
747
+
748
+ grav = grav1*drho
749
+
750
+ # for wavenumbers
751
+ kint1 = (2*np.pi)/(nxt*dx)
752
+ kint2 = (2*np.pi)/(nyt*dy)
753
+ kx2 = kint1*kint1
754
+ ky2 = kint2*kint2
755
+
756
+ # folding frequency:
757
+ mfx = int(nxt/2 + 1)
758
+ mfy = int(nyt/2 + 1)
759
+ mfx2 = mfx*2
760
+ mfy2 = mfy*2
761
+
762
+ # the important part:
763
+ # (1) compute wavenumbers
764
+ # (2) transform topography with fft
765
+ # (3) sum over powers with those wavenumbers
766
+ # (4) upward continue, transform back to space domain, multiply
767
+ # by constants
768
+
769
+ # wavenumbers:
770
+ yj1 = np.arange(1, mfy+1)
771
+ yj = np.append(yj1, yj1[mfy-2:0:-1])
772
+ yyk = (yj-1)*(yj-1)*ky2
773
+
774
+ xi1 = np.arange(1, mfx+1)
775
+ xi = np.append(xi1, xi1[mfx-2:0:-1])
776
+ xxk = (xi-1)*(xi-1)*kx2
777
+
778
+ kwn1 = np.zeros((nxt, nyt))
779
+ for j in range(nyt):
780
+ kwn1[:, j] = np.sqrt(xxk + yyk[j])
781
+
782
+ kwn1 = kwn1.T
783
+
784
+ # fold the data
785
+ if ifold == 1:
786
+ Z = np.vstack((Z, np.flipud(Z))) # fold in X
787
+ Z = np.hstack((Z, np.fliplr(Z))) # fold in Y
788
+
789
+ # first power
790
+ data = np.copy(Z)
791
+ data = np.fft.fft2(data)
792
+ data[0, 0] = 0
793
+
794
+ # sum over the other powers
795
+ csum = np.copy(data) # for adding summation terms
796
+ fact = 1
797
+ for i in range(2, npower+1):
798
+ fact = fact*i
799
+ data = np.copy(Z)**i
800
+ data = np.fft.fft2(data)
801
+
802
+ data = data*(kwn1**(i-1))/fact
803
+ k, l = np.where(kwn1 == 0)
804
+ data[k, l] = 0
805
+
806
+ csum = csum + np.copy(data)
807
+
808
+ # upward continuation
809
+ zlev = slev+dz
810
+ data = csum*grav*np.exp(-zlev*kwn1)
811
+
812
+ data = np.fft.ifft2(data)
813
+ sdata = np.real(data[:ny, :nx]) # back in the spatial domain
814
+
815
+ return sdata
816
+
817
+
818
+ def grav2d_layer_variable_density(rho, dx, dy, z1, z2):
819
+ """
820
+ Calculate the gravity contribution from a layer of equal thickness with
821
+ an inhomogenous density distribution in x and y (homogeneous in z)
822
+
823
+ .. Based on glayer.m by Mark Behn
824
+
825
+ :param rho: 2D density distribution [kg/m^3]
826
+ :type rho: ndarray
827
+ :param dx,dy: sample intervals in km
828
+ :type dx,dy: float
829
+ :param z1,z2: depth to top and bottom of layer in km (both >0)
830
+ :type z1,z2: float
831
+
832
+ :return: (*ndarray*) gravity in mgal
833
+ """
834
+
835
+ si2mg = 1e5
836
+ km2m = 1e3
837
+ G = 6.673e-11
838
+ grav = 2*np.pi*G
839
+
840
+ ny, nx = rho.shape
841
+ dkx = 2*np.pi/(nx*dx)
842
+ dky = 2*np.pi/(ny*dy)
843
+
844
+ ifrho = np.fft.fft2(rho) # take 2D fft of densities
845
+
846
+ crho = np.empty((ny, nx), dtype=complex)
847
+ for j in range(nx):
848
+ for i in range(ny):
849
+ kx, ky = _kvalue(i, j, nx, ny, dkx, dky)
850
+ k = np.sqrt(kx**2 + ky**2)
851
+ if k == 0:
852
+ crho[i, j] = 0
853
+ else:
854
+ crho[i, j] = ifrho[i, j]*grav*(np.exp(-k*z1)-np.exp(-k*z2))/k
855
+
856
+ grho = np.fft.ifft2(crho)
857
+ grho = np.real(grho)*si2mg*km2m
858
+
859
+ return grho
860
+
861
+
862
+ def _kvalue(i, j, nx, ny, dkx, dky):
863
+ """
864
+ Get wavenumber coordinates of one element of a rectangular grid
865
+
866
+ :param i,j: indices in ky,kx directions
867
+ :type i,j: int
868
+ :param nx,ny: dimensions of the grid in the ky,kx directions
869
+ :type nx,ny: int
870
+ :param dkx,dky: sample intervals in kx,ky directions
871
+ :type dkx,dky: float
872
+
873
+ :return: **kx, ky** (*float*) - x and y wavenumbers at (i, j)
874
+ """
875
+
876
+ nyqx = nx/2+1
877
+ nyqy = ny/2+1
878
+
879
+ if j <= nyqx:
880
+ kx = (j)*dkx
881
+ else:
882
+ kx = (j-nx)*dkx
883
+
884
+ if i <= nyqy:
885
+ ky = (i)*dky
886
+ else:
887
+ ky = (i-ny)*dky
888
+ return kx, ky
889
+
890
+
891
+ def therm_halfspace(x, z, u=0.01, Tm=1350, time=False, rhom=3300, rhow=1000,
892
+ a=3.e-5, k=1.e-6):
893
+ """Calculate thermal structure for a half space cooling model.
894
+
895
+ Reference:
896
+
897
+ D. Turcotte & G. Schubert (2014). Geodynamics. Cambridge
898
+ University Press. DOI: 10.1017/CBO9780511843877
899
+ Relevant pages: 161-162, 174-176 in 2nd or 3rd ed?
900
+
901
+ .. Written by Mark D. Behn, November 20, 2003.
902
+ Translated to Python + modded for plate age by Hannah Mark, October 2017
903
+
904
+ :param x: vector of across-axis distance (meters) OR of plate ages (Myr)
905
+ :type x: array_like
906
+ :param z: vector of depth (meters)
907
+ :type z: array_like
908
+ :param u: spreading rate (m/yr)
909
+ :type u: float
910
+ :param time: switch for x vs age input: if ages, set time=True
911
+ and u will be ignored.
912
+ :type time: bool
913
+ :param rhom: mantle density, kg/m^3, default 3300
914
+ :type rhom: float, optional
915
+ :param rhow: water density, kg/m^3, default 1000
916
+ :type rhow: float, optional
917
+ :param a: coefficient of thermal expansion, m^2/sec, default 3e-5
918
+ :type a: float, optional
919
+ :param k: thermal diffusivity, m^2/sec, default 1e-6
920
+ :type k: float, optional
921
+
922
+ :return:
923
+ - **T** (*ndarray*) - gridded temperature over (x, z)
924
+ - **W** (*ndarray*) - seafloor subsidence in meters
925
+ """
926
+
927
+ To = 0 # surface temperature [K]
928
+ # a = 6.e-5 # coeff of thermal expansion [m^2/sec] # used for SCARF calcs (???)
929
+ # k = 2.e-6 # thermal diffusivity [m^2/sec]
930
+
931
+ secyr = 365.25*24*3600 # seconds per year
932
+
933
+ if time:
934
+ x = x*1e6*secyr # convert Myr to sec
935
+ elif not time:
936
+ x = abs(x)/(u/secyr) # convert m to sec
937
+
938
+ X, Z = np.meshgrid(x, z) # grid up x,z pairs
939
+
940
+ # mantle temperature
941
+ T = To + (Tm-To) * erf(Z/(2*(k*X)**.5))
942
+
943
+ # seafloor subsidence
944
+ W = ((2*rhom*a*(Tm-To))/(rhom-rhow)) * \
945
+ (((k*X[0, :])/(np.pi))**.5)
946
+
947
+ return T, W
948
+
949
+
950
+ def therm_Z_halfspace(x, T, u=0.01, Tm=1350, time=False, rhom=3300, rhow=1000,
951
+ a=3.e-5, k=1.e-6):
952
+ """Calculate depth to an isotherm for a half-space cooling model.
953
+
954
+ :param x: vector of across-axis distance [m] OR plate age [Myr]
955
+ :type x: array_like
956
+ :param T: isotherm of choice [K]
957
+ :type T: float
958
+ :param u: spreading rate [m/yr], default 0.01
959
+ :type u: float
960
+ :param time: switch for x vs age input - if time=True,
961
+ u is ignored, default False
962
+ :type time: bool
963
+ :param Tm: mantle potential temperature [K], default 1350
964
+ :type Tm: float, optional
965
+ :param rhom: mantle density, kg/m^3, default 3300
966
+ :type rhom: float, optional
967
+ :param rhow: water density, kg/m^3, default 1000
968
+ :type rhow: float, optional
969
+ :param a: coefficient of thermal expansion, m^2/sec, default 3e-5
970
+ :type a: float, optional
971
+ :param k: thermal diffusivity, m^2/sec, default 1e-6
972
+ :type k: float, optional
973
+
974
+ :returns:
975
+ - **Z** (*ndarray*) - depth of this isotherm below the seafloor in meters
976
+ - **W** (*ndarray*) - seafloor subsidence in meters
977
+ """
978
+
979
+ To = 0 # surface temperature [K]
980
+
981
+ secyr = 365.25*24*3600 # seconds per year
982
+
983
+ if time:
984
+ x = x*1e6*secyr # convert Myr to sec
985
+ elif not time:
986
+ x = abs(x)/(u/secyr) # convert m to sec
987
+
988
+ Z = 2*np.sqrt((k*x))*erfcinv((T-Tm)/(To-Tm))
989
+
990
+ W = ((2*rhom*a*(Tm-To))/(rhom-rhow)) * \
991
+ (((k*x)/(np.pi))**.5)
992
+
993
+ return Z, W
994
+
995
+
996
+ def therm_plate(x, z, u=0.01, zL0=100.e3, Tm=1350, time=False, rhom=3300, rhow=1000,
997
+ a=3.e-5, k=1.e-6):
998
+ """Calculate thermal structure for the plate cooling model.
999
+
1000
+ Reference:
1001
+
1002
+ D. Turcotte & G. Schubert (2014). Geodynamics. Cambridge
1003
+ University Press. DOI: 10.1017/CBO9780511843877
1004
+
1005
+ .. Written by Mark D. Behn, November 20, 2003.
1006
+ Translated to Python + modded for plate age by Hannah Mark, October 2017
1007
+
1008
+ :param x: vector of across-axis distance (meters) OR plate age (Myr)
1009
+ :type x: array_like
1010
+ :param z: vector of depth (meters)
1011
+ :type z: array_like
1012
+ :param u: spreading rate (m/yr), default 0.01
1013
+ :type u: float
1014
+ :param zL0: plate thickness (meters), default 100e3
1015
+ :type zL0: float
1016
+ :param time: switch for x vs age input. If time=True, u is ignored.
1017
+ :type time: bool
1018
+ :param Tm: mantle potential temperature (K), default 1350
1019
+ :type Tm: float, optional
1020
+ :param rhom: mantle density, kg/m^3, default 3300
1021
+ :type rhom: float, optional
1022
+ :param rhow: water density, kg/m^3, default 1000
1023
+ :type rhow: float, optional
1024
+ :param a: coefficient of thermal expansion, m^2/sec, default 3e-5
1025
+ :type a: float, optional
1026
+ :param k: thermal diffusivity, m^2/sec, default 1e-6
1027
+ :type k: float, optional
1028
+
1029
+ :returns:
1030
+ - **T** (*ndarray*) - gridded temperature over (x, z)
1031
+ - **W** (*ndarray*) - seafloor subsidence in meters
1032
+ """
1033
+
1034
+ To = 0 # surface temperature [K]
1035
+
1036
+ secyr = 365.25*24*3600 # seconds per year
1037
+
1038
+ X, Z = np.meshgrid(x, z)
1039
+
1040
+ if time:
1041
+ t = X*1e6*secyr # convert Myr to sec
1042
+ elif not time:
1043
+ t = abs(X)/(u/secyr) # convert across-axis distance to time in SECONDS
1044
+
1045
+ Tterm2 = (2/np.pi)*np.exp(-k*(np.pi**2)*t/(zL0**2))*np.sin(np.pi*Z/zL0)
1046
+ Tterm3 = (1/np.pi)*np.exp(-4*k*(np.pi**2)*t/(zL0**2))*np.sin(2*np.pi*Z/zL0)
1047
+ Tterm4 = (2/np.pi/3)*np.exp(-9*k*(np.pi**2)
1048
+ * t/(zL0**2))*np.sin(3*np.pi*Z/zL0)
1049
+ Tterm5 = (2/np.pi/4)*np.exp(-16*k*(np.pi**2)
1050
+ * t/(zL0**2))*np.sin(4*np.pi*Z/zL0)
1051
+ Tterm6 = (2/np.pi/5)*np.exp(-25*k*(np.pi**2)
1052
+ * t/(zL0**2))*np.sin(5*np.pi*Z/zL0)
1053
+ Tterm7 = (2/np.pi/6)*np.exp(-36*k*(np.pi**2)
1054
+ * t/(zL0**2))*np.sin(6*np.pi*Z/zL0)
1055
+ Tterm8 = (2/np.pi/7)*np.exp(-49*k*(np.pi**2)
1056
+ * t/(zL0**2))*np.sin(7*np.pi*Z/zL0)
1057
+ Tterm9 = (2/np.pi/8)*np.exp(-64*k*(np.pi**2)
1058
+ * t/(zL0**2))*np.sin(8*np.pi*Z/zL0)
1059
+ Tterm10 = (2/np.pi/9)*np.exp(-81*k*(np.pi**2)
1060
+ * t/(zL0**2))*np.sin(9*np.pi*Z/zL0)
1061
+ Tterm11 = (2/np.pi/10)*np.exp(-100*k*(np.pi**2)
1062
+ * t/(zL0**2))*np.sin(10*np.pi*Z/zL0)
1063
+
1064
+ T = To + (Tm - To)*(Z/zL0 + Tterm2 + Tterm3 + Tterm4 + Tterm5 + Tterm6 + Tterm7 +
1065
+ Tterm8 + Tterm9 + Tterm10 + Tterm11)
1066
+
1067
+ mantle = np.where(Z > zL0)[0]
1068
+ T[mantle] = Tm
1069
+
1070
+ Wterm2 = (4/np.pi**2)*np.exp(-k*(np.pi**2)*t[0, :]/(zL0**2))
1071
+ Wterm3 = (4/(9*np.pi**2))*np.exp(-k*9*(np.pi**2)*t[0, :]/(zL0**2))
1072
+ Wterm4 = (4/(25*np.pi**2))*np.exp(-k*25*(np.pi**2)*t[0, :]/(zL0**2))
1073
+
1074
+ W = ((rhom*a*(Tm-To)*zL0)/(rhom-rhow)) * (1./2 - Wterm2 - Wterm3 - Wterm4)
1075
+
1076
+ return T, W
1077
+
1078
+
1079
+ def therm_Z_plate(x, T, u=0.01, zL0=100.e3, Tm=1350, time=False,
1080
+ minz=0, maxz=100e3, zsp=1e2, rhom=3300, rhow=1000,
1081
+ a=3.e-5, k=1.e-6):
1082
+ """Calculate approximate depth to an isotherm in the plate cooling model
1083
+
1084
+ This is done by calculating a temperature field with a decent z spacing
1085
+ and finding the closest points to the isotherm, so it depends strongly
1086
+ on the z spacing that you use. If you need depth to multiple isotherms
1087
+ it's most efficient to get them all at once (using a longer array for
1088
+ T) so the whole temperature field is only calculated one time.
1089
+
1090
+ :param x: array of across-axis distance (meters) OR plate age (Myr)
1091
+ :type x: array_like
1092
+ :param T: temperatures for which you want isotherms (K)
1093
+ :type T: array_like
1094
+ :param u: spreading rate (m/yr), default 0.01
1095
+ :type u: float
1096
+ :param zL0: plate thickness (meters), default 100e3
1097
+ :type zL0: float
1098
+ :param Tm: mantle potential temperature (K), default 1350
1099
+ :type Tm: float, optional
1100
+ :param time: switch for x vs age input. If time=True, u is ignored.
1101
+ :type time: bool
1102
+ :param minz: minimum z for calculating T field (meters), default 0
1103
+ :type minz: float
1104
+ :param maxz: maximum z (meters), default 100e3
1105
+ :type maxz: float
1106
+ :param zps: z spacing for grid (meters), default 1e3
1107
+ :type zps: float
1108
+ :param rhom: mantle density, kg/m^3, default 3300
1109
+ :type rhom: float, optional
1110
+ :param rhow: water density, kg/m^3, default 1000
1111
+ :type rhow: float, optional
1112
+ :param a: coefficient of thermal expansion, m^2/sec, default 3e-5
1113
+ :type a: float, optional
1114
+ :param k: thermal diffusivity, m^2/sec, default 1e-6
1115
+ :type k: float, optional
1116
+
1117
+ :returns: **ziso** (*ndarray*) - depths to isotherms, z(T,x)
1118
+ """
1119
+
1120
+ z = np.arange(minz, maxz, zsp)
1121
+
1122
+ Tp, _ = therm_plate(x, z, u=u, zL0=zL0, Tm=Tm, time=time, rhom=rhom, rhow=rhow,
1123
+ a=a, k=k) # calculate the whole temperature field
1124
+
1125
+ ziso = np.zeros((len(T), len(x)))
1126
+ for i in range(len(T)): # loop isotherms, find depths
1127
+ for j in range(len(x)):
1128
+ ziso[i, j] = z[np.argmin(abs(Tp[:, j]-T[i]))]
1129
+
1130
+ return ziso
1131
+
1132
+ ########################################################################
1133
+ # crustal thickness functions
1134
+ ########################################################################
1135
+
1136
+
1137
+ def crustal_thickness_2D(ur, nx=1000, ny=1, dx=1.3, dy=0, zdown=10, rho=0.4,
1138
+ wlarge=45, wsmall=25, back=False):
1139
+ """
1140
+ Downward continuation of gravity to "topographic relief" ie crustal thickness
1141
+
1142
+ This can be used in 2D, but also works for a single line
1143
+ given ny=1 (which is the default)
1144
+
1145
+ .. Written by Hannah Mark (MIT/WHOI), October 2017
1146
+ Modeled on down_2d.f by Jian Lin (WHOI)
1147
+
1148
+ :param ur: residual gravity anomaly, mgal
1149
+ :type ur: array_like
1150
+ :param nx: number of points in x direction, default 1000
1151
+ :type nx: int
1152
+ :param ny: number of points in y direction, default 1 (>1 for 2D)
1153
+ :type ny: int
1154
+ :param dx: spacing between x points, km, default 1.3
1155
+ :type dx: float
1156
+ :param dy: spacing between y points, km, default 0 (>0 for 2D)
1157
+ :type dy: float
1158
+ :param zdown: downward continuation depth, km, default 10
1159
+ :type zdown: float
1160
+ :param rho: density difference crust to mantle, g/cm^3, default 0.4
1161
+ :type rho: float
1162
+ :param wlarge: max wavelength for taper/cutoff, km, default 45
1163
+ :type wlarge: float
1164
+ :param wsmall: min wavelength for taper/cutoff, km, default 25
1165
+ :type wsmall: float
1166
+ :param back: switch for doing reverse tranform
1167
+ :type back: bool
1168
+
1169
+ :returns:
1170
+ - **crustal thickness** (*ndarray*) - thickness variation in km
1171
+ - **recovered gravity** (*ndarray*) - back-calculated RMBA, (optional, if back=True)
1172
+ """
1173
+ assert wlarge > wsmall, 'wlarge must be larger than wsmall'
1174
+
1175
+ zmin = min(ur)
1176
+ zmax = max(ur)
1177
+ ave = np.mean(ur)
1178
+
1179
+ # shift to a new reference and convert milligal to gal
1180
+ # sign is switched so that positive residual = crustal thinning
1181
+ ur = -0.001*(ur - ave)
1182
+
1183
+ # convert km to cm for spatial params
1184
+ dx = dx*1e5
1185
+ dy = dy*1e5
1186
+ zdown = zdown*1e5
1187
+
1188
+ # calculate wavenumbers for taper/cutoff
1189
+ # we will taper btwn kcut1 and kcut2; cutoff >kcut2
1190
+ wlarge = wlarge*1e5 # convert km to cm
1191
+ wsmall = wsmall*1e5
1192
+ kcut1 = 2*np.pi/wlarge
1193
+ kcut2 = 2*np.pi/wsmall
1194
+ dkcut = kcut2 - kcut1
1195
+ mfx = nx/2 + 1
1196
+ mfy = ny/2 + 1
1197
+
1198
+ prod = 1./(nx*ny) # dimension correction factor
1199
+
1200
+ G = 6.673e-8 # cgs gravity
1201
+ topo1 = 1/(2*np.pi*G*rho) # gravity-to-topography transfer
1202
+
1203
+ kint1 = 2*np.pi/(nx*dx)
1204
+ if dy != 0:
1205
+ kint2 = 2*np.pi/(ny*dy)
1206
+ else:
1207
+ kint2 = 0
1208
+
1209
+ kwn1 = np.zeros((nx, ny)) # compute wavenumbers in 2D
1210
+ for j in range(ny):
1211
+ yj = j
1212
+ if j > mfy:
1213
+ yj = mfy*2 - j
1214
+ yyk = kint2**2 * (yj-1)**2
1215
+
1216
+ for i in range(nx):
1217
+ xi = i
1218
+ if i > mfx:
1219
+ xi = mfx*2 - i
1220
+ xxk = kint1**2 * (xi-1)**2
1221
+
1222
+ kwn1[i, j] = np.sqrt(xxk + yyk)
1223
+
1224
+ # Fourier transform of the gravity residual
1225
+ # this was passed as a 1D array even for a 2D problem
1226
+ ur_arr = ur.reshape(nx, ny)
1227
+ ur_arr_ft = np.fft.fft2(ur_arr) # 2D fft
1228
+
1229
+ # apply wavenumbers, taper
1230
+ for j in range(ny):
1231
+ for i in range(nx):
1232
+ wgt = 1
1233
+ if kwn1[i, j] > kcut2:
1234
+ wgt = 0
1235
+ elif kwn1[i, j] > kcut1 and kwn1[i, j] <= kcut2:
1236
+ t = (kwn1[i, j]-kcut1)/dkcut*np.pi
1237
+ wgt = (np.cos(t)+1)/2
1238
+
1239
+ ur_arr_ft[i, j] = ur_arr_ft[i, j] * \
1240
+ np.exp(kwn1[i, j]*zdown)*topo1*wgt
1241
+
1242
+ # inverse Fourier transform
1243
+ ur_arr_2 = np.fft.fft2(ur_arr_ft)
1244
+
1245
+ # back to vector, correct for dimensions, convert to km
1246
+ ur_arr_2 = ur_arr_2.reshape(-1, 1)
1247
+ ur_arr_2 = ur_arr_2*prod/1e5
1248
+
1249
+ if not back:
1250
+ return ur_arr_2
1251
+ elif back: # reverse the transform and upward continute to check gravity recovery
1252
+ for j in range(ny):
1253
+ for i in range(nx):
1254
+ ur_arr_ft[i, j] = -ur_arr_ft[i, j] * \
1255
+ np.exp(-kwn1[i, j]*zdown)/topo1
1256
+ ur_back = np.fft.fft2(ur_arr_ft)
1257
+ ur_back = ur_back.reshape(-1, 1)
1258
+ ur_back = ur_back*prod*1000+ave
1259
+
1260
+ return ur_arr_2[::-1], ur_back[::-1]