Skip to content

Commit 0dd9319

Browse files
committed
Incorporating ANT Velocity Corrections
* Receiver functions can now be 3D-migrated based on velocities obtained from an ANT model
1 parent 3067d6e commit 0dd9319

File tree

3 files changed

+156
-8
lines changed

3 files changed

+156
-8
lines changed

seismic/misc.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,23 @@ def rtp2xyz(r, theta, phi):
9696
return xout
9797
# end func
9898

99+
def xyz2rtp(x, y, z):
100+
"""
101+
@param x
102+
@param y
103+
@param z
104+
@return: r, theta, phi triplets on a sphere of radius r
105+
t->[0, PI], p->[-PI, PI]
106+
"""
107+
tmp1 = x*x + y*y
108+
tmp2 = tmp1 + z*z
109+
rout = np.zeros((x.shape[0], 3))
110+
rout[:, 0] = np.sqrt(tmp2)
111+
rout[:, 1] = np.arctan2(np.sqrt(tmp1), z)
112+
rout[:, 2] = np.arctan2(y, x)
113+
return rout
114+
# end func
115+
99116
def read_key_value_pairs(file_path:str, keys:list, strict=False)->dict:
100117
"""
101118
Reads a text file containing colon-separated key-value pairs and returns a dictionary with values for specified keys.

seismic/receiver_fn/rf_3dmigrate.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import logging
2020
import click
2121
from seismic.receiver_fn.rf_ccp_util import Migrator
22+
from seismic.receiver_fn.rf_ccp_util import ANTVolume
2223

2324
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
2425
log = logging.getLogger('rf_3dmigrate')
@@ -42,7 +43,15 @@
4243
'that indicates robustness of P-arrival. Typically, a minimum '
4344
'slope-ratio of 5 is able to pick out strong arrivals. The '
4445
'default value of -1 does not apply this filter')
45-
def main(rf_h5_file, output_h5_file, dz, max_depth, fmin, fmax, min_slope_ratio):
46+
@click.option('--ant-model', type=click.Path(exists=True, dir_okay=False),
47+
default=None, show_default=True,
48+
help='ANT model in .txt format to extract Vs from.')
49+
@click.option('--ant-model-max-depth', type=float,
50+
default=100, show_default=True,
51+
help='Maximum depth (km) up to which velocities from the ANT model are to be extracted. '
52+
'Has no impact if --ant-model is not speficied.')
53+
def main(rf_h5_file, output_h5_file, dz, max_depth, fmin, fmax,
54+
min_slope_ratio, ant_model, ant_model_max_depth):
4655
"""Perform 3D migration of RFs
4756
RF_H5_FILE : Path to RFs in H5 format
4857
OUTPUT_H5_FILE: H5 output file name
@@ -52,8 +61,15 @@ def main(rf_h5_file, output_h5_file, dz, max_depth, fmin, fmax, min_slope_ratio)
5261
"""
5362
log.setLevel(logging.DEBUG)
5463

64+
am = None
65+
if(ant_model):
66+
log.info('Loading ANT model: {}'.format(ant_model))
67+
am = ANTVolume(ant_model, ant_model_max_depth)
68+
# end if
69+
5570
m = Migrator(rf_filename=rf_h5_file, dz=dz, max_depth=max_depth,
56-
min_slope_ratio=min_slope_ratio, logger=log)
71+
min_slope_ratio=min_slope_ratio, ant_model=am,
72+
logger=log)
5773
m.process_streams(output_h5_file, fmin=fmin, fmax=fmax)
5874
# end
5975

seismic/receiver_fn/rf_ccp_util.py

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from rf.util import DEG2KM
2222
from obspy.taup import TauPyModel
2323
import h5py
24-
24+
from obspy.geodetics.base import degrees2kilometers
2525
from scipy.interpolate import splev, splrep, sproot, interp1d
2626
from scipy.integrate import simps as simpson
2727
from scipy.signal import hilbert
@@ -34,9 +34,110 @@
3434
from affine import Affine
3535
import struct
3636
from seismic.stream_io import get_obspyh5_index
37-
from seismic.misc import rtp2xyz, split_list
37+
from seismic.misc import rtp2xyz, xyz2rtp, split_list
3838
from seismic.misc_p import parallel_abort
3939

40+
41+
class ANTVolume():
42+
def __init__(self, input_fn, earth_radius=6371, max_depth=100):
43+
"""
44+
input_fn: path to txt file containing the following columns:
45+
Number Lon Lat depth velocity STD VPVS STD XI STD RHO STD
46+
"""
47+
self.input_fn = input_fn
48+
self.earth_radius = earth_radius # km
49+
self.max_depth = max_depth
50+
data = np.loadtxt(input_fn, delimiter=' ', skiprows=1)
51+
52+
self.lons = data[:, 1]
53+
self.lats = data[:, 2]
54+
self.depths_km = data[:, 3]
55+
self.zs = self.earth_radius - self.depths_km
56+
self.vs = data[:, 4]
57+
58+
# clip by max_depth
59+
imask = self.depths_km < max_depth
60+
self.lons = self.lons[imask]
61+
self.lats = self.lats[imask]
62+
self.depths_km = self.depths_km[imask]
63+
self.zs = self.zs[imask]
64+
self.vs = self.vs[imask]
65+
66+
self.xyz = rtp2xyz(self.earth_radius - self.depths_km,
67+
np.radians(90 - self.lats),
68+
np.radians(self.lons))
69+
self.tree = cKDTree(self.xyz)
70+
71+
# nominal grid point separation
72+
self.node_sep_km = degrees2kilometers(np.max([np.median(np.diff(np.unique(self.lats))),
73+
np.median(np.diff(np.unique(self.lats)))]))
74+
# end func
75+
76+
def get_profile(self, lons, lats, max_depth_km=20, ndepths=200, nnbr=10, p=4):
77+
"""
78+
Returns velocities along a vertical profile.
79+
"""
80+
depths = np.linspace(0, max_depth_km, ndepths)
81+
82+
rtp = []
83+
for d in depths:
84+
for lon, lat in zip(lons, lats):
85+
rtp.append([d, lat, lon])
86+
# end for
87+
# end for
88+
rtp = np.array(rtp)
89+
rtp[:, 0] = self.earth_radius - rtp[:, 0]
90+
rtp[:, 1] = np.radians(90 - rtp[:, 1])
91+
rtp[:, 2] = np.radians(rtp[:, 2])
92+
93+
xyz = rtp2xyz(rtp[:, 0], rtp[:, 1], rtp[:, 2])
94+
ds, ids = self.tree.query(xyz, k=nnbr)
95+
96+
if (nnbr > 1):
97+
idw = 1. / np.power(ds, p)
98+
vals = np.sum(idw * np.reshape(self.vs[ids.flatten()], ids.shape), axis=1) / \
99+
np.sum(idw, axis=1)
100+
else:
101+
vals = self.vs[ids]
102+
# end if
103+
104+
return depths, np.reshape(vals, (ndepths, len(lons)))
105+
# end func
106+
107+
def get_velocity(self, lon, lat, depth_km, default=None):
108+
"""
109+
Return the distance to and the velocity of the closest node. The default
110+
value(s) is returned, if the distance to the closest node is larger than
111+
nominal node separation.
112+
"""
113+
114+
rtp = [self.earth_radius - depth_km,
115+
np.radians(90 - lat),
116+
np.radians(lon)]
117+
118+
xyz = None
119+
if (np.all([isinstance(item, np.ndarray) for item in rtp])):
120+
xyz = rtp2xyz(rtp[0], rtp[1], rtp[2])
121+
if (default is not None): assert len(default) == len(rtp[0]), 'Length mismatch detected..'
122+
else:
123+
xyz = rtp2xyz(np.array([rtp[0]]), np.array([rtp[1]]), np.array([rtp[2]]))
124+
if (default is not None): default = np.array([default])
125+
# end if
126+
127+
distances, indices = self.tree.query(xyz, k=1)
128+
result = np.zeros(len(distances))
129+
for i, (d, idx) in enumerate(zip(distances, indices)):
130+
if (d > self.node_sep_km):
131+
result[i] = default[i] if default is not None else 0
132+
else:
133+
result[i] = self.vs[idx]
134+
# end for
135+
136+
if (len(result) == 1): result = result[0]
137+
return result
138+
# end func
139+
# end class
140+
40141
class Gravity:
41142
def __init__(self, gravity_grid_fn):
42143
self._fn = gravity_grid_fn
@@ -52,7 +153,7 @@ def __init__(self, gravity_grid_fn):
52153
self._gt = self._ds.GetGeoTransform()
53154
self._affine_forward_transform = Affine.from_gdal(*self._gt)
54155
self._affine_reverse_transform = ~(self._affine_forward_transform)
55-
# end func
156+
# end func
56157

57158
def _readPixel(self, px, py):
58159
def translateFormat(pt):
@@ -66,7 +167,6 @@ def translateFormat(pt):
66167
GDT_Float64: 'd'
67168
}
68169
return fmttypes.get(pt, 'x')
69-
70170
# end func
71171

72172
structval = self._band.ReadRaster(int(px), int(py), 1, 1, buf_type=self._band.DataType)
@@ -89,11 +189,13 @@ def query(self, lon, lat):
89189
# end class
90190

91191
class Migrator:
92-
def __init__(self, rf_filename, dz, max_depth, min_slope_ratio=-1, earth_radius=6371, logger=None):
192+
def __init__(self, rf_filename, dz, max_depth, min_slope_ratio=-1,
193+
ant_model: ANTVolume = None, earth_radius=6371, logger=None):
93194
self._rf_filename = rf_filename
94195
self._dz = dz
95196
self._max_depth = max_depth
96197
self._min_slope_ratio = min_slope_ratio
198+
self.ant_model = ant_model
97199
self._earth_radius = earth_radius #km
98200
self._logger = logger
99201
# Initialize MPI
@@ -264,6 +366,20 @@ def process_streams(self, output_file, fmin=None, fmax=None, model='iasp91'):
264366
vp = vmodel.evaluate_above(dnew, 'p')
265367
vs = vmodel.evaluate_above(dnew, 's')
266368

369+
# extract velocities from provided ANT model up to a given depth
370+
if(self.ant_model):
371+
imask = dnew < self.ant_model.max_depth
372+
373+
rtp = xyz2rtp(t2xio(d2tio(dnew[imask])),
374+
t2yio(d2tio(dnew[imask])),
375+
t2zio(d2tio(dnew[imask])))
376+
path_depths = self._earth_radius - rtp[:, 0]
377+
path_lats = 90 - np.degrees(rtp[:, 1])
378+
path_lons = np.degrees(rtp[:, 2])
379+
vs[imask] = self.ant_model.get_velocity(path_lons, path_lats,
380+
path_depths, vs[imask])
381+
# end if
382+
267383
#========================================================================
268384
# Create an interpolant for the delay time (t_Ps) of a non-vertically
269385
# incident Ps wave as follows:
@@ -280,7 +396,6 @@ def process_streams(self, output_file, fmin=None, fmax=None, model='iasp91'):
280396
np.sqrt( np.power(vp[:idx], -2.) - p*p), dnew[:idx])
281397
#end for
282398

283-
284399
t.trim(starttime=t.stats.onset, endtime=t.stats.endtime)
285400
ipt.trim(starttime=ipt.stats.onset, endtime=ipt.stats.endtime)
286401

0 commit comments

Comments
 (0)