Source code for obspy.taup.slowness_model

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Slowness model class.
"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
from future.builtins import *  # NOQA

import math

import numpy as np

from .helper_classes import (CriticalDepth, DepthRange, SlownessLayer,
                             SlownessModelError, SplitLayerInfo, TimeDist)
from .slowness_layer import (bullenDepthFor,
                             bullenRadialSlowness, create_from_vlayer,
                             evaluateAtBullen)
from .velocity_layer import (DEFAULT_DENSITY, DEFAULT_QP, DEFAULT_QS,
                             VelocityLayer, evaluateVelocityAtBottom,
                             evaluateVelocityAtTop)


def _fixCriticalDepths(criticalDepths, layerNum, isPWave):
    name = 'pLayerNum' if isPWave else 'sLayerNum'

    mask = criticalDepths[name] > layerNum
    criticalDepths[name][mask] += 1


[docs]class SlownessModel(object): """ Storage and methods for generating slowness-depth pairs. """ DEBUG = False DEFAULT_SLOWNESS_TOLERANCE = 1e-16 radiusOfEarth = 6371.0 # NB if the following are actually cleared (lists are mutable) every # time createSample is called, maybe it would be better to just put these # initialisations into the relevant methods? They do have to be # persistent across method calls in createSample though, so don't. # Stores the layer number for layers in the velocity model with a critical # point at their top. These form the "branches" of slowness sampling. criticalDepths = None # will be list of CriticalDepth objects # Store depth ranges that contains a high slowness zone for P/S. Stored as # DepthRange objects, containing the top depth and bottom depth. highSlownessLayerDepthsP = [] # will be list of DepthRanges highSlownessLayerDepthsS = [] # Stores depth ranges that are fluid, ie S velocity is zero. Stored as # DepthRange objects, containing the top depth and bottom depth. fluidLayerDepths = [] PLayers = None SLayers = None # For methods that have an isPWave parameter SWAVE = False PWAVE = True
[docs] def __init__(self, vMod, minDeltaP=0.1, maxDeltaP=11, maxDepthInterval=115, maxRangeInterval=2.5 * math.pi / 180, maxInterpError=0.05, allowInnerCoreS=True, slowness_tolerance=DEFAULT_SLOWNESS_TOLERANCE, skip_model_creation=False): self.vMod = vMod self.minDeltaP = minDeltaP self.maxDeltaP = maxDeltaP self.maxDepthInterval = maxDepthInterval self.maxRangeInterval = maxRangeInterval self.maxInterpError = maxInterpError self.allowInnerCoreS = allowInnerCoreS self.slowness_tolerance = slowness_tolerance if skip_model_creation: return self.createSample()
[docs] def __str__(self): desc = "".join([ "radiusOfEarth=", str(self.radiusOfEarth), "\n maxDeltaP=", str(self.maxDeltaP), "\n minDeltaP=", str(self.minDeltaP), "\n maxDepthInterval=", str(self.maxDepthInterval), "\n maxRangeInterval=", str(self.maxRangeInterval), "\n allowInnerCoreS=", str(self.allowInnerCoreS), "\n slownessTolerance=", str(self.slowness_tolerance), "\n getNumLayers('P')=", str(self.getNumLayers(self.PWAVE)), "\n getNumLayers('S')=", str(self.getNumLayers(self.SWAVE)), "\n fluidLayerDepths.size()=", str(len(self.fluidLayerDepths)), "\n highSlownessLayerDepthsP.size()=", str(len(self.highSlownessLayerDepthsP)), "\n highSlownessLayerDepthsS.size()=", str(len(self.highSlownessLayerDepthsS)), "\n criticalDepths.size()=", (str(len(self.criticalDepths)) if self.criticalDepths else 'N/A'), "\n"]) desc += "**** Critical Depth Layers ************************\n" desc += str(self.criticalDepths) desc += "\n" desc += "\n**** Fluid Layer Depths ************************\n" for fl in self.fluidLayerDepths: desc += str(fl.topDepth) + "," + str(fl.botDepth) + " " desc += "\n" desc += "\n**** P High Slowness Layer Depths ****************\n" for fl in self.highSlownessLayerDepthsP: desc += str(fl.topDepth) + "," + str(fl.botDepth) + " " desc += "\n" desc += "\n**** S High Slowness Layer Depths ****************\n" for fl in self.highSlownessLayerDepthsS: desc += str(fl.topDepth) + "," + str(fl.botDepth) + " " desc += "\n" desc += "\n**** P Layers ****************\n" for l in self.PLayers: desc += str(l) + "\n" return desc
[docs] def createSample(self): """ Create slowness-depth layers from a velocity model. This method takes a velocity model and creates a vector containing slowness-depth layers that, hopefully, adequately sample both slowness and depth so that the travel time as a function of distance can be reconstructed from the theta function. """ # Some checks on the velocity model self.vMod.validate() if self.vMod.getNumLayers() == 0: raise SlownessModelError("velModel.getNumLayers()==0") if self.vMod.layers[0]['topSVelocity'] == 0: raise SlownessModelError( "Unable to handle zero S velocity layers at surface. " "This should be fixed at some point, but is a limitation of " "TauP at this point.") if self.DEBUG: print("start createSample") self.radiusOfEarth = self.vMod.radiusOfEarth if self.DEBUG: print("findCriticalPoints") self.findCriticalPoints() if self.DEBUG: print("coarseSample") self.coarseSample() if self.DEBUG: self.validate() if self.DEBUG: print("ray_paramCheck") self.ray_paramIncCheck() if self.DEBUG: print("depthIncCheck") self.depthIncCheck() if self.DEBUG: print("distanceCheck") self.distanceCheck() if self.DEBUG: print("fixCriticalPoints") self.fixCriticalPoints() self.validate() if self.DEBUG: print("createSample seems to be done successfully.")
[docs] def findCriticalPoints(self): """ Find all critical points within a velocity model. Critical points are first order discontinuities in velocity/slowness, local extrema in slowness. A high slowness zone is a low velocity zone, but it is possible to have a slightly low velocity zone within a spherical Earth that is not a high slowness zone and thus does not exhibit any of the pathological behavior of a low velocity zone. """ highSlownessZoneP = DepthRange() highSlownessZoneS = DepthRange() fluidZone = DepthRange() inFluidZone = False belowOuterCore = False inHighSlownessZoneP = False inHighSlownessZoneS = False # just some very big values (java had max possible of type, # but these should do) minPSoFar = 1.1e300 minSSoFar = 1.1e300 # First remove any critical points previously stored # so these are effectively re-initialised... it's probably silly self.criticalDepths = np.empty(len(self.vMod.layers), dtype=CriticalDepth) cd_count = 0 self.highSlownessLayerDepthsP = [] # lists of DepthRange self.highSlownessLayerDepthsS = [] self.fluidLayerDepths = [] # Initialize the current velocity layer # to be zero thickness layer with values at the surface currVLayer = self.vMod.layers[0] currVLayer = np.array([( currVLayer['topDepth'], currVLayer['topDepth'], currVLayer['topPVelocity'], currVLayer['topPVelocity'], currVLayer['topSVelocity'], currVLayer['topSVelocity'], currVLayer['topDensity'], currVLayer['topDensity'], currVLayer['topQp'], currVLayer['topQp'], currVLayer['topQs'], currVLayer['topQs'])], dtype=VelocityLayer) currSLayer = create_from_vlayer(currVLayer, self.SWAVE) currPLayer = create_from_vlayer(currVLayer, self.PWAVE) # We know that the top is always a critical slowness so add 0 self.criticalDepths[cd_count] = (0, 0, 0, 0) cd_count += 1 # Check to see if starting in fluid zone. if inFluidZone is False and currVLayer['topSVelocity'] == 0: inFluidZone = True fluidZone = DepthRange(topDepth=currVLayer['topDepth']) currSLayer = currPLayer if minSSoFar > currSLayer['topP']: minSSoFar = currSLayer['topP'] # P is not a typo, it represents slowness, not P-wave speed. if minPSoFar > currPLayer['topP']: minPSoFar = currPLayer['topP'] for layerNum, layer in enumerate(self.vMod.layers): prevVLayer = currVLayer prevSLayer = currSLayer prevPLayer = currPLayer # Could make the following a deep copy, but not necessary. # Also don't just replace layer here and in the loop # control with currVLayer, or the reference to the first # zero thickness layer that has been initialised above # will break. currVLayer = layer # Check again if in fluid zone if inFluidZone is False and currVLayer['topSVelocity'] == 0: inFluidZone = True fluidZone = DepthRange(topDepth=currVLayer['topDepth']) # If already in fluid zone, check if exited (java line 909) if inFluidZone is True and currVLayer['topSVelocity'] != 0: if prevVLayer['botDepth'] > self.vMod.iocbDepth: belowOuterCore = True inFluidZone = False fluidZone.botDepth = prevVLayer['botDepth'] self.fluidLayerDepths.append(fluidZone) currPLayer = create_from_vlayer(currVLayer, self.PWAVE) # If we are in a fluid zone ( S velocity = 0.0 ) or if we are below # the outer core and allowInnerCoreS=false then use the P velocity # structure to look for critical points. if inFluidZone \ or (belowOuterCore and self.allowInnerCoreS is False): currSLayer = currPLayer else: currSLayer = create_from_vlayer(currVLayer, self.SWAVE) if prevSLayer['botP'] != currSLayer['topP'] \ or prevPLayer['botP'] != currPLayer['topP']: # a first order discontinuity self.criticalDepths[cd_count] = ( currSLayer['topDepth'], layerNum, -1, -1) cd_count += 1 if self.DEBUG: print('First order discontinuity, depth =' + str(currSLayer['topDepth'])) print('between' + str(prevPLayer), str(currPLayer)) if inHighSlownessZoneS and currSLayer['topP'] < minSSoFar: if self.DEBUG: print("Top of current layer is the bottom" " of a high slowness zone.") highSlownessZoneS.botDepth = currSLayer['topDepth'] self.highSlownessLayerDepthsS.append(highSlownessZoneS) inHighSlownessZoneS = False if inHighSlownessZoneP and currPLayer['topP'] < minPSoFar: if self.DEBUG: print("Top of current layer is the bottom" " of a high slowness zone.") highSlownessZoneP.botDepth = currSLayer['topDepth'] self.highSlownessLayerDepthsP.append(highSlownessZoneP) inHighSlownessZoneP = False # Update minPSoFar and minSSoFar as all total reflections off # of the top of the discontinuity are ok even though below the # discontinuity could be the start of a high slowness zone. if minPSoFar > currPLayer['topP']: minPSoFar = currPLayer['topP'] if minSSoFar > currSLayer['topP']: minSSoFar = currSLayer['topP'] if inHighSlownessZoneS is False and ( prevSLayer['botP'] < currSLayer['topP'] or currSLayer['topP'] < currSLayer['botP']): # start of a high slowness zone S if self.DEBUG: print("Found S high slowness at first order " + "discontinuity, layer = " + str(layerNum)) inHighSlownessZoneS = True highSlownessZoneS = \ DepthRange(topDepth=currSLayer['topDepth']) highSlownessZoneS.ray_param = minSSoFar if inHighSlownessZoneP is False and ( prevPLayer['botP'] < currPLayer['topP'] or currPLayer['topP'] < currPLayer['botP']): # start of a high slowness zone P if self.DEBUG: print("Found P high slowness at first order " + "discontinuity, layer = " + str(layerNum)) inHighSlownessZoneP = True highSlownessZoneP = \ DepthRange(topDepth=currPLayer['topDepth']) highSlownessZoneP.ray_param = minPSoFar elif ((prevSLayer['topP'] - prevSLayer['botP']) * (prevSLayer['botP'] - currSLayer['botP']) < 0) or ( (prevPLayer['topP'] - prevPLayer['botP']) * (prevPLayer['botP'] - currPLayer['botP'])) < 0: # local slowness extrema, java l 1005 self.criticalDepths[cd_count] = ( currSLayer['topDepth'], layerNum, -1, -1) cd_count += 1 if self.DEBUG: print("local slowness extrema, depth=" + str(currSLayer['topDepth'])) # here is line 1014 of the java src! if inHighSlownessZoneP is False \ and currPLayer['topP'] < currPLayer['botP']: if self.DEBUG: print("start of a P high slowness zone, local " "slowness extrema,minPSoFar= " + str(minPSoFar)) inHighSlownessZoneP = True highSlownessZoneP = \ DepthRange(topDepth=currPLayer['topDepth']) highSlownessZoneP.ray_param = minPSoFar if inHighSlownessZoneS is False \ and currSLayer['topP'] < currSLayer['botP']: if self.DEBUG: print("start of a S high slowness zone, local " "slowness extrema, minSSoFar= " + str(minSSoFar)) inHighSlownessZoneS = True highSlownessZoneS = \ DepthRange(topDepth=currSLayer['topDepth']) highSlownessZoneS.ray_param = minSSoFar if inHighSlownessZoneP and currPLayer['botP'] < minPSoFar: # P: layer contains the bottom of a high slowness zone. java # l 1043 if self.DEBUG: print("layer contains the bottom of a P " + "high slowness zone. minPSoFar=" + str(minPSoFar), currPLayer) highSlownessZoneP.botDepth = self.findDepth_from_layers( minPSoFar, layerNum, layerNum, self.PWAVE) self.highSlownessLayerDepthsP.append(highSlownessZoneP) inHighSlownessZoneP = False if inHighSlownessZoneS and currSLayer['botP'] < minSSoFar: # S: layer contains the bottom of a high slowness zone. java # l 1043 if self.DEBUG: print("layer contains the bottom of a S " + "high slowness zone. minSSoFar=" + str(minSSoFar), currSLayer) # in fluid layers we want to check PWAVE structure # when looking for S wave critical points porS = (self.PWAVE if currSLayer == currPLayer else self.SWAVE) highSlownessZoneS.botDepth = self.findDepth_from_layers( minSSoFar, layerNum, layerNum, porS) self.highSlownessLayerDepthsS.append(highSlownessZoneS) inHighSlownessZoneS = False if minPSoFar > currPLayer['botP']: minPSoFar = currPLayer['botP'] if minPSoFar > currPLayer['topP']: minPSoFar = currPLayer['topP'] if minSSoFar > currSLayer['botP']: minSSoFar = currSLayer['botP'] if minSSoFar > currSLayer['topP']: minSSoFar = currSLayer['topP'] if self.DEBUG and inHighSlownessZoneS: print("In S high slowness zone, layerNum = " + str(layerNum) + " minSSoFar=" + str(minSSoFar)) if self.DEBUG and inHighSlownessZoneP: print("In P high slowness zone, layerNum = " + str(layerNum) + " minPSoFar=" + str(minPSoFar)) # We know that the bottommost depth is always a critical slowness, # so we add vMod.getNumLayers() # java line 1094 self.criticalDepths[cd_count] = (self.radiusOfEarth, self.vMod.getNumLayers(), -1, -1) cd_count += 1 # Check if the bottommost depth is contained within a high slowness # zone, might happen in a flat non-whole-earth model if inHighSlownessZoneS: highSlownessZoneS.botDepth = currVLayer['botDepth'] self.highSlownessLayerDepthsS.append(highSlownessZoneS) if inHighSlownessZoneP: highSlownessZoneP.botDepth = currVLayer['botDepth'] self.highSlownessLayerDepthsP.append(highSlownessZoneP) # Check if the bottommost depth is contained within a fluid zone, this # would be the case if we have a non whole earth model with the bottom # in the outer core or if allowInnerCoreS == false and we want to use # the P velocity structure in the inner core. if inFluidZone: fluidZone['botDepth'] = currVLayer['botDepth'] self.fluidLayerDepths.append(fluidZone) self.criticalDepths = self.criticalDepths[:cd_count] self.validate()
[docs] def getNumLayers(self, isPWave): """ Number of slowness layers. This is meant to return the number of P or S layers. :param isPWave: Return P layer count (``True``) or S layer count (``False``). :type isPWave: bool :returns: Number of slowness layers. :rtype: int """ if isPWave: return len(self.PLayers) else: return len(self.SLayers)
[docs] def findDepth_from_depths(self, ray_param, topDepth, botDepth, isPWave): """ Find depth corresponding to a slowness between two given depths. The given depths are converted to layer numbers before calling :meth:`findDepth_from_layers`. :param ray_param: Slowness (aka ray parameter) to find, in s/km. :type ray_param: float :param topDepth: Top depth to search, in km. :type topDepth: float :param botDepth: Bottom depth to search, in km. :type botDepth: float :param isPWave: ``True`` if P wave or ``False`` for S wave. :type isPWave: bool :returns: Depth (in km) corresponding to the desired slowness. :rtype: float :raises SlownessModelError: If ``topCriticalLayer > botCriticalLayer`` because there are no layers to search, or if there is an increase in slowness, i.e., a negative velocity gradient, that just balances the decrease in slowness due to the spherical Earth, or if the ray parameter ``p`` is not contained within the specified layer range. """ topLayerNum = self.vMod.layerNumberBelow(topDepth)[0] if self.vMod.layers[topLayerNum]['botDepth'] == topDepth: topLayerNum += 1 botLayerNum = self.vMod.layerNumberAbove(botDepth)[0] return self.findDepth_from_layers(ray_param, topLayerNum, botLayerNum, isPWave)
[docs] def findDepth_from_layers(self, p, topCriticalLayer, botCriticalLayer, isPWave): """ Find depth corresponding to a slowness p between two velocity layers. Here, slowness is defined as ``(6731-depth) / velocity``, and sometimes called ray parameter. Both the top and the bottom velocity layers are included. We also check to see if the slowness is less than the bottom slowness of these layers but greater than the top slowness of the next deeper layer. This corresponds to a total reflection. In this case a check needs to be made to see if this is an S wave reflecting off of a fluid layer, use P velocity below in this case. We assume that slowness is monotonic within these layers and therefore there is only one depth with the given slowness. This means we return the first depth that we find. :param p: Slowness (aka ray parameter) to find, in s/km. :type p: float :param topCriticalLayer: Top layer number to search. :type topCriticalLayer: int :param botCriticalLayer: Bottom layer number to search. :type botCriticalLayer: int :param isPWave: ``True`` if P wave or ``False`` for S wave. :type isPWave: bool :returns: Depth (in km) corresponding to the desired slowness. :rtype: float :raises SlownessModelError: If ``topCriticalLayer > botCriticalLayer`` because there are no layers to search, or if there is an increase in slowness, i.e., a negative velocity gradient, that just balances the decrease in slowness due to the spherical Earth, or if the ray parameter ``p`` is not contained within the specified layer range. """ # topP = 1.1e300 # dummy numbers # botP = 1.1e300 waveType = 'P' if isPWave else 'S' if topCriticalLayer > botCriticalLayer: raise SlownessModelError( "findDepth: no layers to search (wrong layer num?)") for layerNum in range(topCriticalLayer, botCriticalLayer + 1): velLayer = self.vMod.layers[layerNum] topVelocity = evaluateVelocityAtTop(velLayer, waveType) botVelocity = evaluateVelocityAtBottom(velLayer, waveType) topP = self.toSlowness(topVelocity, velLayer['topDepth']) botP = self.toSlowness(botVelocity, velLayer['botDepth']) # Check to see if we are within 'chatter level' (numerical # error) of the top or bottom and if so then return that depth. if abs(topP - p) < self.slowness_tolerance: return velLayer['topDepth'] if abs(p - botP) < self.slowness_tolerance: return velLayer['botDepth'] if (topP - p) * (p - botP) >= 0: # Found layer containing p. # We interpolate assuming that velocity is linear within # this interval. So slope is the slope for velocity versus # depth. slope = (botVelocity - topVelocity) / ( velLayer['botDepth'] - velLayer['topDepth']) depth = self.interpolate(p, topVelocity, velLayer['topDepth'], slope) return depth elif layerNum == topCriticalLayer \ and abs(p - topP) < self.slowness_tolerance: # Check to see if p is just outside the topmost layer. If so # then return the top depth. return velLayer['topDepth'] # Is p a total reflection? botP is the slowness at the bottom # of the last velocity layer from the previous loop, set topP # to be the slowness at the top of the next layer. if layerNum < self.vMod.getNumLayers() - 1: velLayer = self.vMod.layers[layerNum + 1] topVelocity = evaluateVelocityAtTop(velLayer, waveType) if (isPWave is False and np.any(self.depthInFluid(velLayer['topDepth']))): # Special case for S waves above a fluid. If top next # layer is in a fluid then we should set topVelocity to # be the P velocity at the top of the layer. topVelocity = evaluateVelocityAtTop(velLayer, 'P') topP = self.toSlowness(topVelocity, velLayer['topDepth']) if botP >= p >= topP: return velLayer['topDepth'] # noinspection PyUnboundLocalVariable if abs(p - botP) < self.slowness_tolerance: # java line 1275 # Check to see if p is just outside the bottommost layer. If so # than return the bottom depth. print(" p is just outside the bottommost layer. This probably " "shouldn't be allowed to happen!") # noinspection PyUnboundLocalVariable return velLayer.getBotDepth() raise SlownessModelError( "slowness p=" + str(p) + "is not contained within the specified layers." + " topCriticalLayer=" + str(topCriticalLayer) + " botCriticalLayer=" + str(botCriticalLayer))
[docs] def toSlowness(self, velocity, depth): """ Convert velocity at some depth to slowness. :param velocity: The velocity to convert, in km/s. :type velocity: float :param depth: The depth (in km) at which to perform the calculation. Must be less than the radius of the Earth defined in this model, or the result is undefined. :type depth: float :returns: The slowness, in s/km. :rtype: float """ if velocity == 0: raise SlownessModelError( "toSlowness: velocity can't be zero, at depth" + str(depth), "Maybe related to using S velocities in outer core?") return (self.radiusOfEarth - depth) / velocity
[docs] def interpolate(self, p, topVelocity, topDepth, slope): """ Interpolate slowness to depth within a layer. We interpolate assuming that velocity is linear within this interval. All parameters must be of the same shape. :param p: The slowness to interpolate, in s/km. :type p: :class:`float` or :class:`~numpy.ndarray` :param topVelocity: The velocity (in km/s) at the top of the layer. :type topVelocity: :class:`float` or :class:`~numpy.ndarray` :param topDepth: The depth (in km) for the top of the layer. :type topDepth: :class:`float` or :class:`~numpy.ndarray` :param slope: The slope (in (km/s)/km) for velocity versus depth. :type slope: :class:`float` or :class:`~numpy.ndarray` :returns: The depth (in km) of the slowness below the layer boundary. :rtype: :class:`float` or :class:`~numpy.ndarray` """ denominator = p * slope + 1 if np.any(denominator == 0): raise SlownessModelError( "Negative velocity gradient that just balances the slowness " "gradient of the spherical slowness, i.e. Earth flattening. " "Instructions unclear; explode.") else: depth = (self.radiusOfEarth + p * (topDepth * slope - topVelocity)) / denominator return depth
[docs] def depthInFluid(self, depth): """ Determine if the given depth is contained within a fluid zone. The fluid zone includes its upper boundary but not its lower boundary. The top and bottom of the fluid zone are not returned as a DepthRange, just like in the Java code, despite its claims to the contrary. :param depth: The depth to check, in km. :type depth: :class:`~numpy.ndarray`, dtype = :class:`float` :returns: ``True`` if the depth is within a fluid zone, ``False`` otherwise. :rtype: :class:`~numpy.ndarray` (dtype = :class:`bool`) """ ret = np.zeros(shape=depth.shape, dtype=np.bool_) for elem in self.fluidLayerDepths: ret |= (elem.topDepth <= depth) & (depth < elem.botDepth) return ret
[docs] def coarseSample(self): """ Create a coarse slowness sampling of the velocity model (vMod). The resultant slowness layers will satisfy the maximum depth increments as well as sampling each point specified within the VelocityModel. The P and S sampling will also be compatible. """ self.PLayers = create_from_vlayer(self.vMod.layers, self.PWAVE) with np.errstate(divide='ignore'): self.SLayers = create_from_vlayer(self.vMod.layers, self.SWAVE) mask = self.depthInFluid(self.vMod.layers['topDepth']) if not self.allowInnerCoreS: mask |= self.vMod.layers['topDepth'] >= self.vMod.iocbDepth self.SLayers[mask] = self.PLayers[mask] # Check for first order discontinuity. However, we only consider # S discontinuities in the inner core if allowInnerCoreS is true. above = self.vMod.layers[:-1] below = self.vMod.layers[1:] mask = np.logical_or( above['botPVelocity'] != below['topPVelocity'], np.logical_and( above['botSVelocity'] != below['topSVelocity'], np.logical_or( self.allowInnerCoreS, below['topDepth'] < self.vMod.iocbDepth))) index = np.where(mask)[0] + 1 above = above[mask] below = below[mask] # If we are going from a fluid to a solid or solid to fluid, e.g., core # mantle or outer core to inner core then we need to use the P velocity # for determining the S discontinuity. topSVel = np.where(above['botSVelocity'] == 0, above['botPVelocity'], above['botSVelocity']) botSVel = np.where(below['topSVelocity'] == 0, below['topPVelocity'], below['topSVelocity']) # Add the layer, with zero thickness but nonzero slowness step, # corresponding to the discontinuity. currVLayer = np.empty(shape=above.shape, dtype=VelocityLayer) currVLayer['topDepth'] = above['botDepth'] currVLayer['botDepth'] = above['botDepth'] currVLayer['topPVelocity'] = above['botPVelocity'] currVLayer['botPVelocity'] = below['topPVelocity'] currVLayer['topSVelocity'] = topSVel currVLayer['botSVelocity'] = botSVel currVLayer['topDensity'].fill(DEFAULT_DENSITY) currVLayer['botDensity'].fill(DEFAULT_DENSITY) currVLayer['topQp'].fill(DEFAULT_QP) currVLayer['botQp'].fill(DEFAULT_QP) currVLayer['topQs'].fill(DEFAULT_QS) currVLayer['botQs'].fill(DEFAULT_QS) currPLayer = create_from_vlayer(currVLayer, self.PWAVE) self.PLayers = np.insert(self.PLayers, index, currPLayer) currSLayer = create_from_vlayer(currVLayer, self.SWAVE) mask2 = (above['botSVelocity'] == 0) & (below['topSVelocity'] == 0) if not self.allowInnerCoreS: mask2 |= currVLayer['topDepth'] >= self.vMod.iocbDepth currSLayer = np.where(mask2, currPLayer, currSLayer) self.SLayers = np.insert(self.SLayers, index, currSLayer) # Make sure that all high slowness layers are sampled exactly # at their bottom for highZone in self.highSlownessLayerDepthsS: sLayerNum = self.layerNumberAbove(highZone.botDepth, self.SWAVE) highSLayer = self.SLayers[sLayerNum] while highSLayer['topDepth'] == highSLayer['botDepth'] and ( (highSLayer['topP'] - highZone.ray_param) * (highZone.ray_param - highSLayer['botP']) < 0): sLayerNum += 1 highSLayer = self.SLayers[sLayerNum] if highZone.ray_param != highSLayer['botP']: self.addSlowness(highZone.ray_param, self.SWAVE) for highZone in self.highSlownessLayerDepthsP: sLayerNum = self.layerNumberAbove(highZone.botDepth, self.PWAVE) highSLayer = self.PLayers[sLayerNum] while highSLayer['topDepth'] == highSLayer['botDepth'] and ( (highSLayer['topP'] - highZone.ray_param) * (highZone.ray_param - highSLayer['botP']) < 0): sLayerNum += 1 highSLayer = self.PLayers[sLayerNum] if highZone.ray_param != highSLayer['botP']: self.addSlowness(highZone.ray_param, self.PWAVE) # Make sure P and S are consistent by adding discontinuities in one to # the other. # Numpy 1.6 compatibility # _tb = self.PLayers[['topP', 'botP']] _tb = np.vstack([self.PLayers['topP'], self.PLayers['botP']]).T.ravel() uniq = np.unique(_tb) for p in uniq: self.addSlowness(p, self.SWAVE) # Numpy 1.6 compatibility # _tb = self.PLayers[['topP', 'botP']] _tb = np.vstack([self.SLayers['topP'], self.SLayers['botP']]).T.ravel() uniq = np.unique(_tb) for p in uniq: self.addSlowness(p, self.PWAVE)
[docs] def layerNumberAbove(self, depth, isPWave): """ Find the index of the slowness layer that contains the given depth. Note that if the depth is a layer boundary, it returns the shallower of the two or possibly more (since total reflections are zero thickness layers) layers. .. seealso:: :meth:`layerNumberBelow` :param depth: The depth to find, in km. :type depth: :class:`float` or :class:`~numpy.ndarray` :param isPWave: Whether to look at P (``True``) velocity or S (``False``) velocity. :type isPWave: bool :returns: The slowness layer containing the requested depth. :rtype: :class:`int` or :class:`~numpy.ndarray` (dtype = :class:`int`, shape = ``depth.shape``) :raises SlownessModelError: If no layer in the slowness model contains the given depth. """ if isPWave: layers = self.PLayers else: layers = self.SLayers # Check to make sure depth is within the range available if np.any(depth < layers[0]['topDepth']) or \ np.any(depth > layers[-1]['botDepth']): raise SlownessModelError("No layer contains this depth") foundLayerNum = np.searchsorted(layers['topDepth'], depth) mask = foundLayerNum != 0 if np.isscalar(foundLayerNum): if mask: foundLayerNum -= 1 else: foundLayerNum[mask] -= 1 return foundLayerNum
[docs] def layerNumberBelow(self, depth, isPWave): """ Find the index of the slowness layer that contains the given depth. Note that if the depth is a layer boundary, it returns the deeper of the two or possibly more (since total reflections are zero thickness layers) layers. .. seealso:: :meth:`layerNumberAbove` :param depth: The depth to find, in km. :type depth: :class:`float` or :class:`~numpy.ndarray` :param isPWave: Whether to look at P (``True``) velocity or S (``False``) velocity. :type isPWave: bool :returns: The slowness layer containing the requested depth. :rtype: :class:`int` or :class:`~numpy.ndarray` (dtype = :class:`int`, shape = ``depth.shape``) :raises SlownessModelError: If no layer in the slowness model contains the given depth. """ if isPWave: layers = self.PLayers else: layers = self.SLayers # Check to make sure depth is within the range available if np.any(depth < layers[0]['topDepth']) or \ np.any(depth > layers[-1]['botDepth']): raise SlownessModelError("No layer contains this depth") foundLayerNum = np.searchsorted(layers['botDepth'], depth, side='right') mask = foundLayerNum == layers.shape[0] if np.isscalar(foundLayerNum): if mask: foundLayerNum -= 1 else: foundLayerNum[mask] -= 1 return foundLayerNum
[docs] def getSlownessLayer(self, layer, isPWave): """ Return the SlownessLayer of the requested wave type. This is not meant to be a clone! :param layer: The number of the layer(s) to return. :type layer: :class:`int` or :class:`~numpy.ndarray` (dtype = :class:`int`) :param isPWave: Whether to return the P layer (``True``) or the S layer (``False``). :type isPWave: bool :returns: The slowness layer(s). :rtype: :class:`~numpy.ndarray` (dtype = :const:`SlownessLayer`, shape = ``layerNum.shape``) """ if isPWave: return self.PLayers[layer] else: return self.SLayers[layer]
[docs] def addSlowness(self, p, isPWave): """ Add a ray parameter to the slowness sampling for the given wave type. Slowness layers are split as needed and P and S sampling are kept consistent within fluid layers. Note, this makes use of the velocity model, so all interpolation is linear in velocity, not in slowness! :param p: The slowness value to add, in s/km. :type p: float :param isPWave: Whether to add to the P wave (``True``) or the S wave (``False``) sampling. :type isPWave: bool """ if isPWave: # NB Unlike Java (unfortunately) these are not modified in place! # NumPy arrays cannot have values inserted in place. layers = self.PLayers otherLayers = self.SLayers wave = 'P' else: layers = self.SLayers otherLayers = self.PLayers wave = 'S' # If depths are the same only need topVelocity, and just to verify we # are not in a fluid. nonzero = layers['topDepth'] != layers['botDepth'] above = self.vMod.evaluateAbove(layers['botDepth'], wave) below = self.vMod.evaluateBelow(layers['topDepth'], wave) topVelocity = np.where(nonzero, below, above) botVelocity = np.where(nonzero, above, below) mask = ((layers['topP'] - p) * (p - layers['botP'])) > 0 # Don't need to check for S waves in a fluid or in inner core if # allowInnerCoreS is False. if not isPWave: mask &= topVelocity != 0 if not self.allowInnerCoreS: iocb_mask = layers['botDepth'] > self.vMod.iocbDepth mask &= ~iocb_mask index = np.where(mask)[0] botDepth = np.copy(layers['botDepth']) # Not a zero thickness layer, so calculate the depth for # the ray parameter. slope = ((botVelocity[nonzero] - topVelocity[nonzero]) / (layers['botDepth'][nonzero] - layers['topDepth'][nonzero])) botDepth[nonzero] = self.interpolate(p, topVelocity[nonzero], layers['topDepth'][nonzero], slope) botLayer = np.empty(shape=index.shape, dtype=SlownessLayer) botLayer['topP'].fill(p) botLayer['topDepth'] = botDepth[mask] botLayer['botP'] = layers['botP'][mask] botLayer['botDepth'] = layers['botDepth'][mask] topLayer = np.empty(shape=index.shape, dtype=SlownessLayer) topLayer['topP'] = layers['topP'][mask] topLayer['topDepth'] = layers['topDepth'][mask] topLayer['botP'].fill(p) topLayer['botDepth'] = botDepth[mask] # numpy 1.6 compatibility otherIndex = np.where(otherLayers.reshape(1, -1) == layers[mask].reshape(-1, 1)) layers[index] = botLayer layers = np.insert(layers, index, topLayer) if len(otherIndex[0]): otherLayers[otherIndex[1]] = botLayer[otherIndex[0]] otherLayers = np.insert(otherLayers, otherIndex[1], topLayer[otherIndex[0]]) if isPWave: self.PLayers = layers self.SLayers = otherLayers else: self.SLayers = layers self.PLayers = otherLayers
[docs] def ray_paramIncCheck(self): """ Check that no slowness layer's ray parameter interval is too large. The limit is determined by ``self.maxDeltaP``. """ for wave in [self.SWAVE, self.PWAVE]: # These might change with calls to addSlowness, so be sure we have # the correct copy. if wave == self.PWAVE: layers = self.PLayers else: layers = self.SLayers diff = layers['topP'] - layers['botP'] absdiff = np.abs(diff) mask = absdiff > self.maxDeltaP diff = diff[mask] absdiff = absdiff[mask] topP = layers['topP'][mask] new_count = np.ceil(absdiff / self.maxDeltaP).astype(np.int_) steps = diff / new_count for start, Np, delta in zip(topP, new_count, steps): for j in range(1, Np): newp = start + j * delta self.addSlowness(newp, self.PWAVE) self.addSlowness(newp, self.SWAVE)
[docs] def depthIncCheck(self): """ Check that no slowness layer is too thick. The maximum is determined by ``self.maxDepthInterval``. """ for wave in [self.SWAVE, self.PWAVE]: # These might change with calls to addSlowness, so be sure we have # the correct copy. if wave == self.PWAVE: layers = self.PLayers else: layers = self.SLayers diff = layers['botDepth'] - layers['topDepth'] mask = diff > self.maxDepthInterval diff = diff[mask] topDepth = layers['topDepth'][mask] new_count = np.ceil(diff / self.maxDepthInterval).astype(np.int_) steps = diff / new_count for start, Nd, delta in zip(topDepth, new_count, steps): new_depth = start + np.arange(1, Nd) * delta if wave == self.SWAVE: velocity = self.vMod.evaluateAbove(new_depth, 'S') smask = velocity == 0 if not self.allowInnerCoreS: smask |= new_depth >= self.vMod.iocbDepth velocity[smask] = self.vMod.evaluateAbove(new_depth[smask], 'P') slowness = self.toSlowness(velocity, new_depth) else: slowness = self.toSlowness( self.vMod.evaluateAbove(new_depth, 'P'), new_depth) for p in slowness: self.addSlowness(p, self.PWAVE) self.addSlowness(p, self.SWAVE)
[docs] def distanceCheck(self): """ Check that no slowness layer is too wide or undersampled. The width must be less than ``self.maxRangeInterval`` and the (estimated) error due to linear interpolation must be less than ``self.maxInterpError``. """ for currWaveType in [self.SWAVE, self.PWAVE]: isCurrOK = False isPrevOK = False prevPrevTD = None prevTD = None currTD = None j = 0 sLayer = self.getSlownessLayer(j, currWaveType) while j < self.getNumLayers(currWaveType): prevSLayer = sLayer sLayer = self.getSlownessLayer(j, currWaveType) if (self.depthInHighSlowness(sLayer['botDepth'], sLayer['botP'], currWaveType) is False and self.depthInHighSlowness(sLayer['topDepth'], sLayer['topP'], currWaveType) is False): # Don't calculate prevTD if we can avoid it if isCurrOK: if isPrevOK: prevPrevTD = prevTD else: prevPrevTD = None prevTD = currTD isPrevOK = True else: prevTD = self.approxDistance(j - 1, sLayer['topP'], currWaveType) isPrevOK = True currTD = self.approxDistance(j, sLayer['botP'], currWaveType) isCurrOK = True # Check for jump of too great distance if (abs(prevTD['dist'] - currTD['dist']) > self.maxRangeInterval and abs(sLayer['topP'] - sLayer['botP']) > 2 * self.minDeltaP): if self.DEBUG: print("At " + str(j) + " Distance jump too great (" ">maxRangeInterval " + str(self.maxRangeInterval) + "), adding " "slowness. ") p = (sLayer['topP'] + sLayer['botP']) / 2 self.addSlowness(p, self.PWAVE) self.addSlowness(p, self.SWAVE) currTD = prevTD prevTD = prevPrevTD else: # Make guess as to error estimate due to linear # interpolation if it is not ok, then we split both # the previous and current slowness layers, this has # the nice, if unintended, consequence of adding # extra samples in the neighborhood of poorly # sampled caustics. splitRayParam = (sLayer['topP'] + sLayer['botP']) / 2 allButLayer = self.approxDistance(j - 1, splitRayParam, currWaveType) splitLayer = np.array([( sLayer['topP'], sLayer['topDepth'], splitRayParam, bullenDepthFor(sLayer, splitRayParam, self.radiusOfEarth))], dtype=SlownessLayer) justLayerTime, justLayerDist = bullenRadialSlowness( splitLayer, splitRayParam, self.radiusOfEarth) splitTD = np.array([( splitRayParam, allButLayer['time'] + 2 * justLayerTime, allButLayer['dist'] + 2 * justLayerDist, 0)], dtype=TimeDist) # Python standard division is not IEEE compliant, # as “The IEEE 754 standard specifies that every # floating point arithmetic operation, including # division by zero, has a well-defined result”. # Use numpy's division instead by using np.array: with np.errstate(divide='ignore', invalid='ignore'): diff = (currTD['time'] - ((splitTD['time'] - prevTD['time']) * ((currTD['dist'] - prevTD['dist']) / (splitTD['dist'] - prevTD['dist'])) + prevTD['time'])) if abs(diff) > self.maxInterpError: p1 = (prevSLayer['topP'] + prevSLayer['botP']) / 2 p2 = (sLayer['topP'] + sLayer['botP']) / 2 self.addSlowness(p1, self.PWAVE) self.addSlowness(p1, self.SWAVE) self.addSlowness(p2, self.PWAVE) self.addSlowness(p2, self.SWAVE) currTD = prevPrevTD isPrevOK = False if j > 0: # Back up one step unless we are at the # beginning, then stay put. j -= 1 sLayer = self.getSlownessLayer( j - 1 if j - 1 >= 0 else 0, currWaveType) # This sLayer will become prevSLayer in the # next loop. else: isPrevOK = False isCurrOK = False else: j += 1 if self.DEBUG and j % 10 == 0: print(j) else: prevPrevTD = None prevTD = None currTD = None isCurrOK = False isPrevOK = False j += 1 if self.DEBUG and j % 100 == 0: print(j) if self.DEBUG: print("Number of " + ("P" if currWaveType else "S") + " slowness layers: " + str(j))
[docs] def depthInHighSlowness(self, depth, ray_param, isPWave): """ Determine if depth and slowness are within a high slowness zone. Whether the high slowness zone includes its upper boundary and its lower boundaries depends upon the ray parameter. The slowness at the depth is needed because if depth happens to correspond to a discontinuity that marks the bottom of the high slowness zone but the ray is actually a total reflection then it is not part of the high slowness zone. The ray parameter that delimits the zone, i.e., it can turn at the top and the bottom, is in the zone at the top, but out of the zone at the bottom. (?) NOTE: I changed this method a bit by throwing out some seemingly useless copying of the values in tempRange, which I think are not used anywhere else. :param depth: The depth to check, in km. :type depth: float :param ray_param: The slowness to check, in s/km. :type ray_param: float :param isPWave: Whether to check the P wave (``True``) or the S wave (``False``). :type isPWave: bool :returns: ``True`` if within a high slowness zone, ``False`` otherwise. :rtype: bool """ if isPWave: highSlownessLayerDepths = self.highSlownessLayerDepthsP else: highSlownessLayerDepths = self.highSlownessLayerDepthsS for tempRange in highSlownessLayerDepths: if tempRange.topDepth <= depth <= tempRange.botDepth: if ray_param > tempRange.ray_param \ or (ray_param == tempRange.ray_param and depth == tempRange.topDepth): return True return False
[docs] def approxDistance(self, slownessTurnLayer, p, isPWave): """ Approximate distance for ray turning at the bottom of a layer. Generates approximate distance, in radians, for a ray from a surface source that turns at the bottom of the given slowness layer. :param slownessTurnLayer: The number of the layer at which the ray should turn. :type slownessTurnLayer: int :param p: The slowness to calculate, in s/km. :type p: float :param isPWave: Whether to use the P (``True``) or S (``False``) wave. :type isPWave: bool :returns: The time (in s) and distance (in rad) the ray travels. :rtype: :class:`~numpy.ndarray` (dtype = :const:`TimeDist`, shape = (``slownessTurnLayer``, )) """ # First, if the slowness model contains less than slownessTurnLayer # elements we can't calculate a distance. if slownessTurnLayer >= self.getNumLayers(isPWave): raise SlownessModelError( "Can't calculate a distance when getNumLayers() is smaller " "than the given slownessTurnLayer!") if p < 0: raise SlownessModelError("Ray parameter must not be negative!") td = np.zeros(1, dtype=TimeDist) td['p'] = p layerNum = np.arange(0, slownessTurnLayer + 1) if len(layerNum): time, dist = self.layerTimeDist(p, layerNum, isPWave) # Return 2* distance and time because there is a downgoing as well # as an upgoing leg, which are equal since this is for a surface # source. td['time'] = 2 * np.sum(time) td['dist'] = 2 * np.sum(dist) return td
[docs] def layerTimeDist(self, sphericalRayParam, layerNum, isPWave, check=True): """ Calculate time and distance for a ray passing through a layer. Calculates the time and distance increments accumulated by a ray of spherical ray parameter ``p`` when passing through layer ``layerNum``. Note that this gives half of the true range and time increments since there will be both an upgoing and a downgoing path. It also only does the calculation for the simple cases of the centre of the Earth, where the ray parameter is zero, or for constant velocity layers. Otherwise, it calls :func:`~.bullenRadialSlowness`. Either ``sphericalRayParam`` or ``layerNum`` must be 0-D, or they must have the same shape. :param sphericalRayParam: The spherical ray parameter of the ray(s), in s/km. :type sphericalRayParam: :class:`float` or :class:`~numpy.ndarray` :param layerNum: The layer(s) in which the calculation should be done. :type layerNum: :class:`float` or :class:`~numpy.ndarray` :param isPWave: Whether to look at the P (``True``) or S (``False``) wave. :type isPWave: bool :param check: Whether to perform checks of input consistency. :type check: bool :returns: The time (in s) and distance (in rad) increments for the specified ray(s) and layer(s). :rtype: :class:`~numpy.ndarray` (dtype = :const:`TimeDist`, shape = ``sphericalRayParam.shape`` or ``layerNum.shape``) :raises SlownessModelError: If the ray with the given spherical ray parameter cannot propagate within this layer, or if the ray turns within this layer but not at the bottom. These checks may be bypassed by specifying ``check=False``. """ sphericalLayer = self.getSlownessLayer(layerNum, isPWave) # First make sure that a ray with this ray param can propagate # within this layer and doesn't turn in the middle of the layer. If # not, raise error. if check and sphericalRayParam > max(np.max(sphericalLayer['topP']), np.max(sphericalLayer['botP'])): raise SlownessModelError("Ray cannot propagate within this layer, " "given ray param too large.") if np.any(sphericalRayParam < 0): raise SlownessModelError("Ray parameter must not be negative!") if check and sphericalRayParam > min(np.min(sphericalLayer['topP']), np.min(sphericalLayer['botP'])): raise SlownessModelError("Ray turns in the middle of this layer! " "layerNum = " + str(layerNum)) pdim = np.ndim(sphericalRayParam) ldim = np.ndim(layerNum) if ldim == 1 and pdim == 0: time = np.empty(shape=layerNum.shape, dtype=np.float_) dist = np.empty(shape=layerNum.shape, dtype=np.float_) elif ldim == 0 and pdim == 1: time = np.empty(shape=sphericalRayParam.shape, dtype=np.float_) dist = np.empty(shape=sphericalRayParam.shape, dtype=np.float_) elif ldim == pdim and (ldim == 0 or layerNum.shape == sphericalRayParam.shape): time = np.empty(shape=layerNum.shape, dtype=np.float_) dist = np.empty(shape=layerNum.shape, dtype=np.float_) else: raise TypeError('Either sphericalRayParam or layerNum must be 0D, ' 'or they must have the same shape.') # Check to see if this layer has zero thickness, if so then it is # from a critically reflected slowness sample. That means just # return 0 for time and distance increments. zero_thick = sphericalLayer['topDepth'] == sphericalLayer['botDepth'] if ldim == 0: if zero_thick: time.fill(0) dist.fill(0) return time, dist else: zero_thick = np.zeros(shape=time.shape, dtype=np.bool_) leftover = ~zero_thick time[zero_thick] = 0 dist[zero_thick] = 0 # Check to see if this layer contains the centre of the Earth. If so # then the spherical ray parameter should be 0.0 and we calculate the # range and time increments using a constant velocity layer (sphere). # See eqns. 43 and 44 of [Buland1983]_, although we implement them # slightly differently. Note that the distance and time increments are # for just downgoing or just upgoing, i.e. from the top of the layer # to the centre of the earth or vice versa but not both. This is in # keeping with the convention that these are one way distance and time # increments. We will multiply the result by 2 at the end, or if we are # doing a 1.5D model, the other direction may be different. The time # increment for a ray of zero ray parameter passing half way through a # sphere of constant velocity is just the spherical slowness at the top # of the sphere. An amazingly simple result! centre_layer = np.logical_and(leftover, np.logical_and( sphericalRayParam == 0, sphericalLayer['botDepth'] == self.radiusOfEarth)) leftover &= ~centre_layer if np.any(layerNum[centre_layer] != self.getNumLayers(isPWave) - 1): raise SlownessModelError("There are layers deeper than the " "centre of the Earth!") time[centre_layer] = sphericalLayer['topP'][centre_layer] dist[centre_layer] = math.pi / 2 # Now we check to see if this is a constant velocity layer and if so # than we can do a simple triangle calculation to get the range and # time increments. To get the time increment we first calculate the # path length through the layer using the law of cosines, noting # that the angle at the top of the layer can be obtained from the # spherical Snell's Law. The time increment is just the path length # divided by the velocity. To get the distance we first find the # angular distance traveled, using the law of sines. topRadius = self.radiusOfEarth - sphericalLayer['topDepth'] botRadius = self.radiusOfEarth - sphericalLayer['botDepth'] with np.errstate(invalid='ignore'): vel = botRadius / sphericalLayer['botP'] constant_velocity = np.logical_and( leftover, np.abs(topRadius / sphericalLayer['topP'] - vel) < self.slowness_tolerance) leftover &= ~constant_velocity topRadius = topRadius[constant_velocity] botRadius = botRadius[constant_velocity] vel = vel[constant_velocity] if pdim: ray_param_const_velocity = sphericalRayParam[constant_velocity] else: ray_param_const_velocity = sphericalRayParam # In cases of a ray turning at the bottom of the layer numerical # round-off can cause botTerm to be very small (1e-9) but # negative which causes the sqrt to raise an error. We check for # values that are within the numerical chatter of zero and just # set them to zero. topTerm = topRadius ** 2 - (ray_param_const_velocity * vel) ** 2 topTerm[np.abs(topTerm) < self.slowness_tolerance] = 0 # In this case the ray turns at the bottom of this layer so # sphericalRayParam*vel == botRadius, and botTerm should be # zero. We check for this case specifically because # numerical chatter can cause small round-off errors that # lead to botTerm being negative, causing a sqrt error. botTerm = np.zeros(shape=topTerm.shape) mask = (ray_param_const_velocity != sphericalLayer['botP'][constant_velocity]) if pdim: botTerm[mask] = botRadius[mask] ** 2 - ( ray_param_const_velocity[mask] * vel[mask]) ** 2 else: botTerm[mask] = botRadius[mask] ** 2 - ( ray_param_const_velocity * vel[mask]) ** 2 b = np.sqrt(topTerm) - np.sqrt(botTerm) time[constant_velocity] = b / vel dist[constant_velocity] = np.arcsin( b * ray_param_const_velocity * vel / (topRadius * botRadius)) # If the layer is not a constant velocity layer or the centre of the # Earth and p is not zero we have to do it the hard way: time[leftover], dist[leftover] = bullenRadialSlowness( sphericalLayer[leftover] if ldim else sphericalLayer, sphericalRayParam[leftover] if pdim else sphericalRayParam, self.radiusOfEarth, check=check) if check and (np.any(time < 0) or np.any(np.isnan(time)) or np.any(dist < 0) or np.any(np.isnan(dist))): raise SlownessModelError( "layer time|dist < 0 or NaN.") return time, dist
[docs] def fixCriticalPoints(self): """ Reset the slowness layers that correspond to critical points. """ self.criticalDepths['pLayerNum'] = self.layerNumberBelow( self.criticalDepths['depth'], self.PWAVE) sLayer = self.getSlownessLayer(self.criticalDepths['pLayerNum'], self.PWAVE) # We want the last critical point to be the bottom of the last layer. mask = ((self.criticalDepths['pLayerNum'] == len(self.PLayers) - 1) & (sLayer['botDepth'] == self.criticalDepths['depth'])) self.criticalDepths['pLayerNum'][mask] += 1 self.criticalDepths['sLayerNum'] = self.layerNumberBelow( self.criticalDepths['depth'], self.SWAVE) sLayer = self.getSlownessLayer(self.criticalDepths['sLayerNum'], self.SWAVE) # We want the last critical point to be the bottom of the last layer. mask = ((self.criticalDepths['sLayerNum'] == len(self.SLayers) - 1) & (sLayer['botDepth'] == self.criticalDepths['depth'])) self.criticalDepths['sLayerNum'][mask] += 1
[docs] def validate(self): """ Perform consistency check on the slowness model. In Java, there is a separate validate method defined in the SphericalSModel subclass and as such overrides the validate in SlownessModel, but it itself calls the super method (by super.validate()), i.e. the code above. Both are merged here (in fact, it only contained one test). """ if self.radiusOfEarth <= 0: raise SlownessModelError("Radius of Earth must be positive.") if self.maxDepthInterval <= 0: raise SlownessModelError( "maxDepthInterval must be positive and non-zero.") # Check for inconsistencies in high slowness zones. for isPWave in [self.PWAVE, self.SWAVE]: if isPWave: highSlownessLayerDepths = self.highSlownessLayerDepthsP else: highSlownessLayerDepths = self.highSlownessLayerDepthsS prevDepth = -1e300 for highSZoneDepth in highSlownessLayerDepths: if highSZoneDepth.topDepth >= highSZoneDepth.botDepth: raise SlownessModelError( "High Slowness zone has zero or negative thickness!") if highSZoneDepth.topDepth <= prevDepth: raise SlownessModelError( "High Slowness zone overlaps previous zone.") prevDepth = highSZoneDepth.botDepth # Check for inconsistencies in fluid zones. prevDepth = -1e300 for fluidZone in self.fluidLayerDepths: if fluidZone.topDepth >= fluidZone.botDepth: raise SlownessModelError( "Fluid zone has zero or negative thickness!") if fluidZone.topDepth <= prevDepth: raise SlownessModelError("Fluid zone overlaps previous zone.") prevDepth = fluidZone.botDepth # Check for inconsistencies in slowness layers. for layers in [self.PLayers, self.SLayers]: if layers is None: continue if np.any(np.isnan(layers['topP']) | np.isnan(layers['botP'])): raise SlownessModelError("Slowness layer has NaN values.") if np.any((layers['topP'] < 0) | (layers['botP'] < 0)): raise SlownessModelError( "Slowness layer has negative slowness.") if np.any(layers['topP'][1:] != layers['botP'][:-1]): raise SlownessModelError( "Slowness layer slowness does not agree with " "previous layer (at same depth)!") if np.any(layers['topDepth'] > layers['botDepth']): raise SlownessModelError( "Slowness layer has negative thickness.") if layers['topDepth'][0] > 0: raise SlownessModelError("Gap between slowness layers!") if np.any(layers['topDepth'][1:] > layers['botDepth'][:-1]): raise SlownessModelError("Gap between slowness layers!") if layers['topDepth'][0] < 0: raise SlownessModelError("Slowness layer overlaps previous!") if np.any(layers['topDepth'][1:] < layers['botDepth'][:-1]): raise SlownessModelError("Slowness layer overlaps previous!") if np.any(np.isnan(layers['topDepth']) | np.isnan(layers['botDepth'])): raise SlownessModelError( "Slowness layer depth (top or bottom) is NaN!") if np.any(layers['botDepth'] > self.radiusOfEarth): raise SlownessModelError( "Slowness layer has a depth larger than radius of the " "Earth.") # Everything seems OK. return True
[docs] def getMinTurnRayParam(self, depth, isPWave): """ Find minimum slowness, turning but not reflected, at or above a depth. Normally this is the slowness sample at the given depth, but if the depth is within a high slowness zone, then it may be smaller. :param depth: The depth to search for, in km. :type depth: float :param isPWave: Whether to search the P (``True``) or S (``False``) wave. :type isPWave: bool :returns: The minimum ray parameter, in s/km. :rtype: float """ minPSoFar = 1e300 if self.depthInHighSlowness(depth, 1e300, isPWave): for sLayer in (self.PLayers if isPWave else self.SLayers): if sLayer['botDepth'] == depth: minPSoFar = min(minPSoFar, sLayer['botP']) return minPSoFar elif sLayer['botDepth'] > depth: minPSoFar = min(minPSoFar, evaluateAtBullen(sLayer, depth, self.radiusOfEarth)) return minPSoFar else: minPSoFar = min(minPSoFar, sLayer['botP']) else: sLayer = self.getSlownessLayer( self.layerNumberAbove(depth, isPWave), isPWave) if depth == sLayer['botDepth']: minPSoFar = sLayer['botP'] else: minPSoFar = evaluateAtBullen(sLayer, depth, self.radiusOfEarth) return minPSoFar
[docs] def getMinRayParam(self, depth, isPWave): """ Find minimum slowness, turning or reflected, at or above a depth. Normally this is the slowness sample at the given depth, but if the depth is within a high slowness zone, then it may be smaller. Also, at first order discontinuities, there may be many slowness samples at the same depth. :param depth: The depth to search for, in km. :type depth: float :param isPWave: Whether to search the P (``True``) or S (``False``) wave. :type isPWave: bool :returns: The minimum ray parameter, in s/km. :rtype: float """ minPSoFar = self.getMinTurnRayParam(depth, isPWave) sLayerAbove = self.getSlownessLayer( self.layerNumberAbove(depth, isPWave), isPWave) sLayerBelow = self.getSlownessLayer( self.layerNumberBelow(depth, isPWave), isPWave) if sLayerAbove['botDepth'] == depth: minPSoFar = min(minPSoFar, sLayerAbove['botP'], sLayerBelow['topP']) return minPSoFar
[docs] def splitLayer(self, depth, isPWave): """ Split a slowness layer into two slowness layers. The interpolation for splitting a layer is a Bullen p=Ar^B and so does not directly use information from the VelocityModel. :param depth: The depth at which attempt a split, in km. :type depth: float :param isPWave: Whether to split based on P (``True``) or S (``False``) wave. :type isPWave: bool :returns: Information about the split as (or if) it was performed, such that: * ``neededSplit=True`` if a layer was actually split; * ``movedSample=True`` if a layer was very close, and so moving the layer's depth is better than making a very thin layer; * ``ray_param=...``, the new ray parameter (in s/km), if the layer was split. :rtype: :class:`~.SplitLayerInfo` """ layerNum = self.layerNumberAbove(depth, isPWave) sLayer = self.getSlownessLayer(layerNum, isPWave) if sLayer['topDepth'] == depth or sLayer['botDepth'] == depth: # Depth is already on a slowness layer boundary so no need to # split any slowness layers. return SplitLayerInfo(self, False, False, 0) elif abs(sLayer['topDepth'] - depth) < 0.000001: # Check for very thin layers, just move the layer to hit the # boundary. outLayers = self.PLayers if isPWave else self.SLayers outLayers[layerNum] = np.array([(sLayer['topP'], depth, sLayer['botP'], sLayer['botDepth'])], dtype=SlownessLayer) sLayer = self.getSlownessLayer(layerNum - 1, isPWave) outLayers[layerNum - 1] = np.array([(sLayer['topP'], sLayer['topDepth'], sLayer['botP'], depth)], dtype=SlownessLayer) out = self out.PLayers = outLayers if isPWave else self.PLayers out.SLayers = outLayers if isPWave else self.SLayers return SplitLayerInfo(out, False, True, sLayer['botP']) elif abs(depth - sLayer['botDepth']) < 0.000001: # As above. outLayers = self.PLayers if isPWave else self.SLayers outLayers[layerNum] = np.array([(sLayer['topP'], sLayer['topDepth'], sLayer['botP'], depth)], dtype=SlownessLayer) sLayer = self.getSlownessLayer(layerNum + 1, isPWave) outLayers[layerNum + 1] = np.array([(sLayer['topP'], depth, sLayer['botP'], sLayer['botDepth'])], dtype=SlownessLayer) out = self out.PLayers = outLayers if isPWave else self.PLayers out.SLayers = outLayers if isPWave else self.SLayers return SplitLayerInfo(out, False, True, sLayer['botP']) else: # Must split properly. p = evaluateAtBullen(sLayer, depth, self.radiusOfEarth) topLayer = np.array([(sLayer['topP'], sLayer['topDepth'], p, depth)], dtype=SlownessLayer) botLayer = np.array([(p, depth, sLayer['botP'], sLayer['botDepth'])], dtype=SlownessLayer) outLayers = self.PLayers if isPWave else self.SLayers outLayers[layerNum] = botLayer outLayers = np.insert(outLayers, layerNum, topLayer) # Fix critical layers since we added a slowness layer. outCriticalDepths = self.criticalDepths _fixCriticalDepths(outCriticalDepths, layerNum, isPWave) if isPWave: outPLayers = outLayers outSLayers = self._fixOtherLayers(self.SLayers, p, sLayer, topLayer, botLayer, outCriticalDepths, False) else: outPLayers = self._fixOtherLayers(self.PLayers, p, sLayer, topLayer, botLayer, outCriticalDepths, True) outSLayers = outLayers out = self out.criticalDepths = outCriticalDepths out.PLayers = outPLayers out.SLayers = outSLayers return SplitLayerInfo(out, True, False, p)
[docs] def _fixOtherLayers(self, otherLayers, p, changedLayer, newTopLayer, newBotLayer, criticalDepths, isPWave): """ Fix other wave layers when a split is made. This performs the second split of the *other* wave type when a split is made by :meth:`splitLayer`. """ out = otherLayers # Make sure to keep sampling consistent. If in a fluid, both wave # types will share a single slowness layer. otherIndex = np.where(otherLayers == changedLayer) if len(otherIndex[0]): out[otherIndex[0]] = newBotLayer out = np.insert(out, otherIndex[0], newTopLayer) for otherLayerNum, sLayer in enumerate(out.copy()): if (sLayer['topP'] - p) * (p - sLayer['botP']) > 0: # Found a slowness layer with the other wave type that # contains the new slowness sample. topLayer = np.array([(sLayer['topP'], sLayer['topDepth'], p, bullenDepthFor(sLayer, p, self.radiusOfEarth))], dtype=SlownessLayer) botLayer = np.array([(p, topLayer['botDepth'], sLayer['botP'], sLayer['botDepth'])], dtype=SlownessLayer) out[otherLayerNum] = botLayer out = np.insert(out, otherLayerNum, topLayer) # Fix critical layers since we have added a slowness layer. _fixCriticalDepths(criticalDepths, otherLayerNum, not isPWave) # Skip next layer as it was just added: achieved by slicing # the list iterator. return out