Source code for obspy.taup.tau_model

# -*- coding: utf-8 -*-
"""
Internal TauModel class.
"""
from collections import OrderedDict
from pathlib import Path
from copy import deepcopy
from itertools import count
from math import pi

import numpy as np

from .helper_classes import DepthRange, SlownessModelError, TauModelError
from .slowness_model import SlownessModel
from .tau_branch import TauBranch
from .velocity_model import VelocityModel


[docs] class TauModel(object): """ Provides storage of all the TauBranches comprising a model. """
[docs] def __init__(self, s_mod, radius_of_planet, is_spherical=True, cache=None, debug=False, skip_calc=False): self.debug = debug # Depth for which tau model as constructed. self.source_depth = 0.0 self.radius_of_planet = radius_of_planet # True if this is a spherical slowness model. False if flat. self.is_spherical = is_spherical # Ray parameters used to construct the tau branches. This may only be # a subset of the slownesses/ray parameters saved in the slowness # model due to high slowness zones (low velocity zones). self.ray_params = None # 2D NumPy array containing a TauBranch object # corresponding to each "branch" of the tau model, First list is P, # second is S. Branches correspond to depth regions between # discontinuities or reversals in slowness gradient for a wave type. # Each branch contains time, distance, and tau increments for each ray # parameter in ray_param for the layer. Rays that turn above the branch # layer get 0 for time, distance, and tau increments. self.tau_branches = None self.s_mod = s_mod # Branch with the source at its top. self.source_branch = 0 # Depths that should not have reflections or phase conversions. For # instance, if the source is not at a branch boundary then # no_discon_depths contains source depth and reflections and phase # conversions are not allowed at this branch boundary. If the source # happens to fall on a real discontinuity then it is not included. self.no_discon_depths = [] if cache is None: self._depth_cache = OrderedDict() elif cache is not False: self._depth_cache = cache else: self._depth_cache = None if not skip_calc: self.calc_tau_inc_from()
[docs] def calc_tau_inc_from(self): """ Calculates tau for each branch within a slowness model. """ # First, we must have at least 1 slowness layer to calculate a # distance. Otherwise we must signal an exception. if self.s_mod.get_num_layers(True) == 0 \ or self.s_mod.get_num_layers(False) == 0: raise SlownessModelError( "Can't calculate tauInc when get_num_layers() = 0. " "I need more slowness samples.") self.s_mod.validate() # Create an array holding the ray parameter that we will use for # constructing the tau splines. Only store ray parameters that are # not in a high slowness zone, i.e. they are smaller than the # minimum ray parameter encountered so far. num_branches = len(self.s_mod.critical_depths) - 1 self.tau_branches = np.empty((2, num_branches), dtype=TauBranch) # Here we find the list of ray parameters to be used for the tau # model. We only need to find ray parameters for S waves since P # waves have been constructed to be a subset of the S samples. ray_num = 0 min_p_so_far = self.s_mod.s_layers[0]['top_p'] temp_ray_params = np.empty(2 * self.s_mod.get_num_layers(False) + len(self.s_mod.critical_depths)) # Make sure we get the top slowness of the very top layer temp_ray_params[ray_num] = min_p_so_far ray_num += 1 for curr_s_layer in self.s_mod.s_layers: # Add the top if it is strictly less than the last sample added. # Note that this will not be added if the slowness is continuous # across the layer boundary. if curr_s_layer['top_p'] < min_p_so_far: temp_ray_params[ray_num] = curr_s_layer['top_p'] ray_num += 1 min_p_so_far = curr_s_layer['top_p'] if curr_s_layer['bot_p'] < min_p_so_far: # Add the bottom if it is strictly less than the last sample # added. This will always happen unless we are # within a high slowness zone. temp_ray_params[ray_num] = curr_s_layer['bot_p'] ray_num += 1 min_p_so_far = curr_s_layer['bot_p'] # Copy tempRayParams to ray_param while chopping off trailing zeros # (from the initialisation), so the size is exactly right. NB # slicing doesn't really mean deep copy, but it works for a list of # doubles like this self.ray_params = temp_ray_params[:ray_num] if self.debug: print("Number of slowness samples for tau:" + str(ray_num)) for wave_num, is_p_wave in enumerate([True, False]): # The minimum slowness seen so far. min_p_so_far = self.s_mod.get_slowness_layer(0, is_p_wave)['top_p'] # for critNum, (topCritDepth, botCritDepth) in enumerate(zip( # self.s_mod.critical_depths[:-1], # self.s_mod.critical_depths[1:])): # Faster: for crit_num, top_crit_depth, bot_crit_depth in zip( count(), self.s_mod.critical_depths[:-1], self.s_mod.critical_depths[1:]): top_crit_layer_num = top_crit_depth['p_layer_num'] \ if is_p_wave else top_crit_depth['s_layer_num'] bot_crit_layer_num = ( bot_crit_depth['p_layer_num'] if is_p_wave else bot_crit_depth['s_layer_num']) - 1 self.tau_branches[wave_num, crit_num] = \ TauBranch(top_crit_depth['depth'], bot_crit_depth['depth'], is_p_wave) self.tau_branches[wave_num, crit_num].debug = self.debug self.tau_branches[wave_num, crit_num].create_branch( self.s_mod, min_p_so_far, self.ray_params) # Update minPSoFar. Note that the new minPSoFar could be at # the start of a discontinuity over a high slowness zone, # so we need to check the top, bottom and the layer just # above the discontinuity. top_s_layer = self.s_mod.get_slowness_layer(top_crit_layer_num, is_p_wave) bot_s_layer = self.s_mod.get_slowness_layer(bot_crit_layer_num, is_p_wave) min_p_so_far = min( min_p_so_far, min(top_s_layer['top_p'], bot_s_layer['bot_p'])) bot_s_layer = self.s_mod.get_slowness_layer( self.s_mod.layer_number_above(bot_crit_depth['depth'], is_p_wave), is_p_wave) min_p_so_far = min(min_p_so_far, bot_s_layer['bot_p']) # Here we decide which branches are the closest to the Moho, CMB, # and IOCB by comparing the depth of the top of the branch with the # depths in the Velocity Model. best_moho = 1e300 best_cmb = 1e300 best_iocb = 1e300 for branch_num, t_branch in enumerate(self.tau_branches[0]): if abs(t_branch.top_depth - self.s_mod.v_mod.moho_depth) <= \ best_moho: # Branch with Moho at its top. self.moho_branch = branch_num best_moho = abs(t_branch.top_depth - self.s_mod.v_mod.moho_depth) if abs(t_branch.top_depth - self.s_mod.v_mod.cmb_depth) < best_cmb: self.cmb_branch = branch_num best_cmb = abs(t_branch.top_depth - self.s_mod.v_mod.cmb_depth) if abs(t_branch.top_depth - self.s_mod.v_mod.iocb_depth) < \ best_iocb: self.iocb_branch = branch_num best_iocb = abs(t_branch.top_depth - self.s_mod.v_mod.iocb_depth) # check bottom of last layer, zero radius, in case no core for # cmb and iocb t_branch = self.tau_branches[0, -1] if abs(t_branch.bot_depth - self.s_mod.v_mod.cmb_depth) < best_cmb: self.cmb_branch = len(self.tau_branches[0]) self.cmb_depth = t_branch.bot_depth if abs(t_branch.bot_depth - self.s_mod.v_mod.iocb_depth) < best_iocb: self.iocb_branch = len(self.tau_branches[0]) self.iocb_depth = t_branch.bot_depth # Now set moho_depth etc. to the top of the branches we have decided # on. self.moho_depth = self.tau_branches[0, self.moho_branch].top_depth if (self.cmb_branch < len(self.tau_branches[0])): self.cmb_depth = self.tau_branches[0, self.cmb_branch].top_depth if (self.iocb_branch < len(self.tau_branches[0])): self.iocb_depth = self.tau_branches[0, self.iocb_branch].top_depth self.validate()
[docs] def __str__(self): desc = "Delta tau for each slowness sample and layer.\n" for j, ray_param in enumerate(self.ray_params): for i, tb in enumerate(self.tau_branches[0]): desc += ( " i " + str(i) + " j " + str(j) + " ray_param " + str(ray_param) + " tau " + str(tb.tau[j]) + " time " + str(tb.time[j]) + " dist " + str(tb.dist[j]) + " degrees " + str(tb.dist[j] * 180 / pi) + "\n") desc += "\n" return desc
[docs] def validate(self): # Could implement the model validation; not critical right now return True
[docs] def depth_correct(self, depth): """ Called in TauPTime. Computes a new tau model for a source at depth using the previously computed branches for a surface source. No change is needed to the branches above and below the branch containing the depth, except for the addition of a slowness sample. The branch containing the source depth is split into 2 branches, and up going branch and a downgoing branch. Additionally, the slowness at the source depth must be sampled exactly as it is an extremal point for each of these branches. Cf. [Buland1983]_, page 1290. """ if self.source_depth != 0: raise TauModelError("Can't depth correct a TauModel that is not " "originally for a surface source.") if depth > self.radius_of_planet: raise TauModelError("Can't depth correct to a source deeper than " "the radius of the planet.") return self.load_from_depth_cache(depth)
[docs] def load_from_depth_cache(self, depth): # Very simple and straightforward LRU cache implementation. if self._depth_cache is not None: # Retrieve and later insert again to get LRU cache behaviour. try: value = self._depth_cache.pop(depth) except KeyError: value = self._load_from_depth_cache(depth) self._depth_cache[depth] = value # Pop first key-value pairs until at most 128 elements are still # in the cache. while len(self._depth_cache) > 128: self._depth_cache.popitem(last=False) return value else: return self._load_from_depth_cache(depth)
[docs] def _load_from_depth_cache(self, depth): depth_corrected = self.split_branch(depth) depth_corrected.source_depth = depth depth_corrected.source_branch = depth_corrected.find_branch(depth) depth_corrected.validate() return depth_corrected
[docs] def split_branch(self, depth): """ Returns a new TauModel with the branches containing depth split at depth. Used for putting a source at depth since a source can only be located on a branch boundary. """ # First check to see if depth happens to already be a branch # boundary, then just return original model. for tb in self.tau_branches[0]: if tb.top_depth == depth or tb.bot_depth == depth: return deepcopy(self) # Depth is not a branch boundary, so must modify the tau model. index_p = -1 p_wave_ray_param = -1 index_s = -1 s_wave_ray_param = -1 out_s_mod = self.s_mod out_ray_params = self.ray_params # Do S wave first since the S ray param is > P ray param. for is_p_wave in [False, True]: split_info = out_s_mod.split_layer(depth, is_p_wave) out_s_mod = split_info.s_mod if split_info.needed_split and not split_info.moved_sample: # Split the slowness layers containing depth into two layers # each. new_ray_param = split_info.ray_param # Insert the new ray parameters into the ray_param array. above = out_ray_params[:-1] below = out_ray_params[1:] index = (above > new_ray_param) & (new_ray_param > below) if np.any(index): index = np.where(index)[0][0] + 1 out_ray_params = np.insert(out_ray_params, index, new_ray_param) if is_p_wave: index_p = index p_wave_ray_param = new_ray_param else: index_s = index s_wave_ray_param = new_ray_param # Now add a sample to each branch above the depth, split the branch # containing the depth, and add a sample to each deeper branch. branch_to_split = self.find_branch(depth) new_tau_branches = np.empty((2, self.tau_branches.shape[1] + 1), dtype=TauBranch) for i in range(branch_to_split): new_tau_branches[0, i] = deepcopy(self.tau_branches[0, i]) new_tau_branches[1, i] = deepcopy(self.tau_branches[1, i]) # Add the new ray parameter(s) from splitting the S and/or P # wave slowness layer to both the P and S wave tau branches (if # splitting occurred). if index_s != -1: new_tau_branches[0, i].insert(s_wave_ray_param, out_s_mod, index_s) new_tau_branches[1, i].insert(s_wave_ray_param, out_s_mod, index_s) if index_p != -1: new_tau_branches[0, i].insert(p_wave_ray_param, out_s_mod, index_p) new_tau_branches[1, i].insert(p_wave_ray_param, out_s_mod, index_p) for p_or_s in range(2): new_tau_branches[p_or_s, branch_to_split] = TauBranch( self.tau_branches[p_or_s, branch_to_split].top_depth, depth, p_or_s == 0) new_tau_branches[p_or_s, branch_to_split].create_branch( out_s_mod, self.tau_branches[p_or_s, branch_to_split].max_ray_param, out_ray_params) new_tau_branches[p_or_s, branch_to_split + 1] = \ self.tau_branches[p_or_s, branch_to_split].difference( new_tau_branches[p_or_s, branch_to_split], index_p, index_s, out_s_mod, new_tau_branches[p_or_s, branch_to_split].min_ray_param, out_ray_params) for i in range(branch_to_split + 1, len(self.tau_branches[0])): for p_or_s in range(2): new_tau_branches[p_or_s, i + 1] = \ deepcopy(self.tau_branches[p_or_s, i]) if index_s != -1: # Add the new ray parameter from splitting the S wave # slownes layer to both the P and S wave tau branches. for p_or_s in range(2): new_tau_branches[p_or_s, i + 1].insert( s_wave_ray_param, out_s_mod, index_s) if index_p != -1: # Add the new ray parameter from splitting the P wave # slownes layer to both the P and S wave tau branches. for p_or_s in range(2): new_tau_branches[p_or_s, i + 1].insert( p_wave_ray_param, out_s_mod, index_p) # We have split a branch so possibly source_branch, moho_branch, # cmb_branch and iocb_branch are off by 1. out_source_branch = self.source_branch if self.source_depth > depth: out_source_branch += 1 out_moho_branch = self.moho_branch if self.moho_depth > depth: out_moho_branch += 1 out_cmb_branch = self.cmb_branch if self.cmb_depth > depth: out_cmb_branch += 1 out_iocb_branch = self.iocb_branch if self.iocb_depth > depth: out_iocb_branch += 1 # No overloaded constructors - so do it this way to bypass the # calc_tau_inc_from in the __init__. tau_model = TauModel( out_s_mod, radius_of_planet=out_s_mod.v_mod.radius_of_planet, is_spherical=self.is_spherical, cache=False, debug=self.debug, skip_calc=True) tau_model.source_depth = self.source_depth tau_model.source_branch = out_source_branch tau_model.moho_branch = out_moho_branch tau_model.moho_depth = self.moho_depth tau_model.cmb_branch = out_cmb_branch tau_model.cmb_depth = self.cmb_depth tau_model.iocb_branch = out_iocb_branch tau_model.iocb_depth = self.iocb_depth tau_model.ray_params = out_ray_params tau_model.tau_branches = new_tau_branches tau_model.no_discon_depths = self.no_discon_depths + [depth] tau_model.validate() return tau_model
[docs] def find_branch(self, depth): """Finds the branch that either has the depth as its top boundary, or strictly contains the depth. Also, we allow the bottom-most branch to contain its bottom depth, so that the center of the planet is contained within the bottom branch.""" for i, tb in enumerate(self.tau_branches[0]): if tb.top_depth <= depth < tb.bot_depth: return i # Check to see if depth is centre of the planet. if self.tau_branches[0, -1].bot_depth == depth: return len(self.tau_branches) - 1 else: raise TauModelError("No TauBranch contains this depth.")
[docs] def get_tau_branch(self, branch_nu, is_p_wave): if is_p_wave: return self.tau_branches[0, branch_nu] else: return self.tau_branches[1, branch_nu]
[docs] def get_branch_depths(self): """ Return an array of the depths that are boundaries between branches. :return: """ branch_depths = [self.get_tau_branch(0, True).top_depth] branch_depths += [ self.get_tau_branch(i - 1, True).bot_depth for i in range(1, len(self.tau_branches[0]))] return branch_depths
[docs] def serialize(self, filename): """ Serialize model to numpy npz binary file. Summary of contents that have to be handled during serialization:: TauModel ======== cmb_branch <type 'int'> cmb_depth <type 'float'> debug <type 'bool'> iocb_branch <type 'int'> iocb_depth <type 'float'> moho_branch <type 'int'> moho_depth <type 'float'> no_discon_depths <type 'list'> (of float!?) radius_of_planet <type 'float'> ray_params <type 'numpy.ndarray'> (1D, float) s_mod <class 'obspy.taup.slowness_model.SlownessModel'> source_branch <type 'int'> source_depth <type 'float'> is_spherical <type 'bool'> tau_branches <type 'numpy.ndarray'> (2D, type TauBranch) TauBranch ========= debug <type 'bool'> bot_depth <type 'float'> dist <type 'numpy.ndarray'> is_p_wave <type 'bool'> max_ray_param <type 'float'> min_ray_param <type 'float'> min_turn_ray_param <type 'float'> tau <type 'numpy.ndarray'> time <type 'numpy.ndarray'> top_depth <type 'float'> SlownessModel ============= debug <type 'bool'> p_layers <type 'numpy.ndarray'> p_wave <type 'bool'> s_layers <type 'numpy.ndarray'> s_wave <type 'bool'> allow_inner_core_s <type 'bool'> critical_depths <type 'numpy.ndarray'> fluid_layer_depths <type 'list'> (of DepthRange) high_slowness_layer_depths_p <type 'list'> (of DepthRange) high_slowness_layer_depths_s <type 'list'> (of DepthRange) max_delta_p <type 'float'> max_depth_interval <type 'float'> max_interp_error <type 'float'> max_range_interval <type 'float'> min_delta_p <type 'float'> radius_of_planet <type 'float'> slowness_tolerance <type 'float'> v_mod <class 'obspy.taup.velocity_model.VelocityModel'> VelocityModel ============= cmb_depth <type 'float'> iocb_depth <type 'float'> is_spherical <type 'bool'> layers <type 'numpy.ndarray'> max_radius <type 'float'> min_radius <type 'int'> model_name <type 'unicode'> moho_depth <type 'float'> radius_of_planet <type 'float'> """ # a) handle simple contents keys = ['cmb_branch', 'cmb_depth', 'debug', 'iocb_branch', 'iocb_depth', 'moho_branch', 'moho_depth', 'no_discon_depths', 'radius_of_planet', 'ray_params', 'source_branch', 'source_depth', 'is_spherical'] arrays = {k: getattr(self, k) for k in keys} # b) handle .tau_branches i, j = self.tau_branches.shape for j_ in range(j): for i_ in range(i): # just store the shape of self.tau_branches in the key names # for later reconstruction of array in deserialization. key = 'tau_branches__%i/%i__%i/%i' % (j_, j, i_, i) arrays[key] = self.tau_branches[i_][j_]._to_array() # c) handle simple contents of .s_mod dtypes = [('debug', np.bool_), ('p_wave', np.bool_), ('s_wave', np.bool_), ('allow_inner_core_s', np.bool_), ('max_delta_p', np.float64), ('max_depth_interval', np.float64), ('max_interp_error', np.float64), ('max_range_interval', np.float64), ('min_delta_p', np.float64), ('radius_of_planet', np.float64), ('slowness_tolerance', np.float64)] slowness_model = np.empty(shape=(), dtype=dtypes) for dtype in dtypes: key = dtype[0] slowness_model[key] = getattr(self.s_mod, key) arrays['s_mod'] = slowness_model # d) handle complex contents of .s_mod arrays['s_mod.p_layers'] = self.s_mod.p_layers arrays['s_mod.s_layers'] = self.s_mod.s_layers arrays['s_mod.critical_depths'] = self.s_mod.critical_depths for key in ['fluid_layer_depths', 'high_slowness_layer_depths_p', 'high_slowness_layer_depths_s']: data = getattr(self.s_mod, key) if len(data) == 0: arr_ = np.array([]) else: arr_ = np.vstack([data_._to_array() for data_ in data]) arrays['s_mod.' + key] = arr_ # e) handle .s_mod.v_mod dtypes = [('cmb_depth', np.float64), ('iocb_depth', np.float64), ('is_spherical', np.bool_), ('max_radius', np.float64), ('min_radius', np.int_), ('model_name', np.str_, len(self.s_mod.v_mod.model_name)), ('moho_depth', np.float64), ('radius_of_planet', np.float64)] velocity_model = np.empty(shape=(), dtype=dtypes) for dtype in dtypes: key = dtype[0] velocity_model[key] = getattr(self.s_mod.v_mod, key) arrays['v_mod'] = velocity_model arrays['v_mod.layers'] = self.s_mod.v_mod.layers # finally save the collection of (structured) arrays to a binary file np.savez_compressed(filename, **arrays)
[docs] @staticmethod def deserialize(filename, cache=None): """ Deserialize model from numpy npz binary file. """ with np.load(filename) as npz: model = TauModel(s_mod=None, radius_of_planet=float(npz["radius_of_planet"]), cache=cache, skip_calc=True) complex_contents = [ 'tau_branches', 's_mod', 'v_mod', 's_mod.p_layers', 's_mod.s_layers', 's_mod.critical_depths', 's_mod.fluid_layer_depths', 's_mod.high_slowness_layer_depths_p', 's_mod.high_slowness_layer_depths_s', 'v_mod.layers'] # a) handle simple contents for key in npz.keys(): # we have multiple, dynamic key names for individual tau # branches now, skip them all if key in complex_contents or key.startswith('tau_branches'): continue arr = npz[key] if arr.ndim == 0: arr = arr[()] setattr(model, key, arr) # b) handle .tau_branches tau_branch_keys = [key for key in npz.keys() if key.startswith('tau_branches_')] j, i = tau_branch_keys[0].split("__")[1:] i = int(i.split("/")[1]) j = int(j.split("/")[1]) branches = np.empty(shape=(i, j), dtype=np.object_) for key in tau_branch_keys: j_, i_ = key.split("__")[1:] i_ = int(i_.split("/")[0]) j_ = int(j_.split("/")[0]) branches[i_][j_] = TauBranch._from_array(npz[key]) # no idea how numpy lays out empty arrays of object type, # make a copy just in case.. branches = np.copy(branches) setattr(model, "tau_branches", branches) # c) handle simple contents of .s_mod slowness_model = SlownessModel(v_mod=None, skip_model_creation=True) setattr(model, "s_mod", slowness_model) for key in npz['s_mod'].dtype.names: # restore scalar types from 0d array arr = npz['s_mod'][key] if arr.ndim == 0: arr = arr.flatten()[0] setattr(slowness_model, key, arr) # d) handle complex contents of .s_mod for key in ['p_layers', 's_layers', 'critical_depths']: setattr(slowness_model, key, npz['s_mod.' + key]) for key in ['fluid_layer_depths', 'high_slowness_layer_depths_p', 'high_slowness_layer_depths_s']: arr_ = npz['s_mod.' + key] if len(arr_) == 0: data = [] else: data = [DepthRange._from_array(x) for x in arr_] setattr(slowness_model, key, data) # e) handle .s_mod.v_mod velocity_model = VelocityModel( model_name=npz["v_mod"]["model_name"], radius_of_planet=float(npz["v_mod"]["radius_of_planet"]), min_radius=float(npz["v_mod"]["min_radius"]), max_radius=float(npz["v_mod"]["max_radius"]), moho_depth=float(npz["v_mod"]["moho_depth"]), cmb_depth=float(npz["v_mod"]["cmb_depth"]), iocb_depth=float(npz["v_mod"]["iocb_depth"]), is_spherical=bool(npz["v_mod"]["is_spherical"]), layers=None ) setattr(slowness_model, "v_mod", velocity_model) setattr(velocity_model, 'layers', npz['v_mod.layers']) return model
[docs] @staticmethod def from_file(model_name, cache=None): model_path = Path(model_name) if model_path.exists(): filepath = model_path else: filepath = Path(__file__).parent / "data" filepath = filepath / (model_name.lower()+".npz") return TauModel.deserialize(filepath, cache=cache)