#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Objects and functions dealing with seismic phases.
"""
from __future__ import (absolute_import, division, print_function,
unicode_literals)
from future.builtins import * # NOQA
from itertools import count
import math
import re
import numpy as np
from .helper_classes import Arrival, TauModelError, TimeDist
from .c_wrappers import clibtau
[docs]class SeismicPhase(object):
"""
Stores and transforms seismic phase names to and from their
corresponding sequence of branches. Will maybe contain "expert" mode
wherein paths may start in the core. Principal use is to calculate leg
contributions for scattered phases. Nomenclature: "K" - downgoing wave
from source in core; "k" - upgoing wave from source in core.
"""
DEBUG = False
# Enables phases originating in core.
expert = False
# Used by addToBranch when the path turns within a segment. We assume
# that no ray will turn downward so turning implies turning from
# downward to upward, ie U.
TURN = 0
# Used by addToBranch when the path reflects off the top of the end of a
# segment, ie ^.
REFLECTTOP = 1
# Used by addToBranch when the path reflects off the bottom of the end
# of a segment, ie v.
REFLECTBOT = 2
# Used by addToBranch when the path transmits up through the end of a
# segment.
TRANSUP = 3
# Used by addToBranch when the path transmits down through the end of a
# segment.
TRANSDOWN = 4
# The maximum degrees that a Pn or Sn can refract along the moho. Note
# this is not the total distance, only the segment along the moho. The
# default is 20 degrees.
maxRefraction = 20
# The maximum degrees that a Pdiff or Sdiff can diffract along the CMB.
# Note this is not the total distance, only the segment along the CMB.
# The default is 60 degrees.
maxDiffraction = 60
[docs] def __init__(self, name, tMod):
# Minimum/maximum ray parameters that exist for this phase.
self.minRayParam = None
self.maxRayParam = None
# Index within TauModel.ray_param that corresponds to maxRayParam.
# Note that maxRayParamIndex < minRayParamIndex as ray parameter
# decreases with increasing index.
self.maxRayParamIndex = -1
# Index within TauModel.ray_param that corresponds to minRayParam.
# Note that maxRayParamIndex < minRayParamIndex as ray parameter
# decreases with increasing index.
self.minRayParamIndex = -1
# Temporary branch numbers determining where to start adding to the
# branch sequence.
self.currBranch = None
# Temporary end action so we know what we did at the end of the last
# section of the branch sequence.
# Used in addToBranch() and parseName().
self.endAction = None
# The phase name, e.g. PKiKP.
self.name = name
# The source depth within the TauModel that was used to generate
# this phase.
self.source_depth = tMod.source_depth
# TauModel to generate phase for.
self.tMod = tMod
# Array of distances corresponding to the ray parameters stored in
# ray_param.
self.dist = None
# Array of times corresponding to the ray parameters stored in
# ray_param.
self.time = None
# Array of possible ray parameters for this phase.
self.ray_param = None
# The minimum distance that this phase can be theoretically observed.
self.minDistance = 0.0
# The maximum distance that this phase can be theoretically observed.
self.maxDistance = 1e300
# List (could make array!) of branch numbers for the given phase.
# Note that this depends upon both the earth model and the source
# depth.
self.branchSeq = []
# True if the current leg of the phase is down going. This allows a
# check to make sure the path is correct.
# Used in addToBranch() and parseName().
self.downGoing = []
# ArrayList of wave types corresponding to each leg of the phase.
self.waveType = []
# List containing strings for each leg.
self.legs = leg_puller(name)
# Name with depths corrected to be actual discontinuities in the model.
self.puristName = self.create_purist_name(tMod)
self.parse_name(tMod)
self.sum_branches(tMod)
[docs] def create_purist_name(self, tMod):
currLeg = self.legs[0]
# Deal with surface wave veocities first, since they are a special
# case.
if len(self.legs) == 2 and currLeg.endswith("kmps"):
puristName = self.name
return puristName
puristName = ""
# Only loop to penultimate element as last leg is always "END".
for currLeg in self.legs[:-1]:
# Find out if the next leg represents a phase conversion or
# reflection depth.
if currLeg[0] in "v^":
disconBranch = closest_branch_to_depth(tMod, currLeg[1])
legDepth = tMod.tauBranches[0, disconBranch].topDepth
puristName += currLeg[0]
puristName += str(legDepth)
else:
try:
float(currLeg)
except ValueError:
# If currLeg is just a string:
puristName += currLeg
else:
# If it is indeed a number:
disconBranch = closest_branch_to_depth(tMod, currLeg)
legDepth = tMod.tauBranches[0, disconBranch].topDepth
puristName += str(legDepth)
return puristName
[docs] def parse_name(self, tMod):
"""
Construct a branch sequence from the given phase name and tau model.
"""
currLeg = self.legs[0]
nextLeg = currLeg
isPWave = True
# Deal with surface wave velocities first, since they are a special
# case.
if len(self.legs) == 2 and currLeg.endswith("kmps"):
return
# Make a check for J legs if the model doesn't allow J:
if "J" in self.name and not tMod.sMod.allowInnerCoreS:
raise TauModelError("J phases are not created for this model: {}"
.format(self.name))
# Set currWave to be the wave type for this leg, P or S
if currLeg in ("p", "K", "k") or currLeg[0] == "P":
isPWave = True
elif currLeg in ("s", "J") or currLeg[0] == "S":
isPWave = False
# First, decide whether the ray is upgoing or downgoing from the
# source. If it is up going then the first branch number would be
# model.sourceBranch-1 and downgoing would be model.sourceBranch.
if currLeg[0] in "sS":
# Exclude S sources in fluids.
sdep = tMod.source_depth
if tMod.cmbDepth < sdep < tMod.iocbDepth:
self.maxRayParam, self.minRayParam = -1, -1
return
if currLeg[0] in "PS" or (self.expert and currLeg[0] in "KI"):
# Downgoing from source.
self.currBranch = tMod.sourceBranch
# Treat initial downgoing as if it were an underside reflection.
self.endAction = self.REFLECTBOT
elif currLeg in ("p", "s") or (self.expert and currLeg[0] == "k"):
# Upgoing from source: treat initial downgoing as if it were a
# topside reflection.
self.endAction = self.REFLECTTOP
if tMod.sourceBranch != 0:
self.currBranch = tMod.sourceBranch - 1
else:
# p and s for zero source depth are only at zero distance
# and then can be called P or S.
self.maxRayParam = -1
self.minRayParam = -1
return
else:
raise TauModelError(
"First phase not recognised {}: ".format(currLeg) +
"Must be one of P, Pg, Pn, Pdiff, p or the S equivalents.")
# Set maxRayParam to be a horizontal ray leaving the source and set
# minRayParam to be a vertical (p=0) ray.
if tMod.sourceBranch != 0:
self.maxRayParam = max(
tMod.getTauBranch(tMod.sourceBranch - 1,
isPWave).minTurnRayParam,
tMod.getTauBranch(tMod.sourceBranch, isPWave).maxRayParam)
else:
self.maxRayParam = tMod.getTauBranch(tMod.sourceBranch,
isPWave).maxRayParam
self.minRayParam = 0
isLegDepth, isNextLegDepth = False, False
self.endAction = self.TRANSDOWN
# Now loop over all the phase legs and construct the proper branch
# sequence.
currLeg = "START" # So the prevLeg isn't wrong on the first pass.
for legNum in range(len(self.legs) - 1):
prevLeg = currLeg
currLeg = nextLeg
nextLeg = self.legs[legNum + 1]
isLegDepth = isNextLegDepth
# Find out if the next leg represents a phase conversion depth.
try:
nextLegDepth = float(nextLeg)
isNextLegDepth = True
except ValueError:
nextLegDepth = -1
isNextLegDepth = False
# Set currWave to be the wave type for this leg, "P" or "S".
isPWavePrev = isPWave
if currLeg in ("p", "k", "I") or currLeg[0] == "P":
isPWave = True
elif currLeg in ("s", "J") or currLeg[0] == "S":
isPWave = False
elif currLeg == "K":
# Here we want to use whatever isPWave was on the last leg
# so do nothing. This makes sure we us the correct
# maxRayParam from the correct TauBranch within the outer
# core. In other words K has a high slowness zone if it
# entered the outer core as a mantle P wave, but doesn't if
# it entered as a mantle S wave. It shouldn't matter for
# inner core to outer core type legs.
pass
# Check to see if there has been a phase conversion.
if len(self.branchSeq) > 0 and isPWavePrev != isPWave:
self.phase_conversion(tMod, self.branchSeq[-1],
self.endAction, isPWavePrev)
# Deal with p and s case first.
if currLeg in ("p", "s", "k"):
if nextLeg[0] == "v":
raise TauModelError(
"p and s must always be upgoing and cannot come "
"immediately before a top-sided reflection.")
elif nextLeg.startswith("^"):
disconBranch = closest_branch_to_depth(tMod, nextLeg[1])
if self.currBranch >= disconBranch:
self.add_to_branch(tMod, self.currBranch, disconBranch,
isPWave, self.REFLECTTOP)
else:
raise TauModelError(
"Phase not recognised: {currLeg} followed by "
"{nextLeg} when currBranch > disconBranch".format(
**locals()))
elif nextLeg == "m" and self.currBranch >= tMod.mohoBranch:
self.add_to_branch(tMod, self.currBranch, tMod.mohoBranch,
isPWave, self.TRANSUP)
elif nextLeg[0] in ("P", "S") or nextLeg in ("K", "END"):
disconBranch = tMod.cmbBranch if nextLeg == "K" else 0
self.add_to_branch(
tMod, self.currBranch, disconBranch, isPWave,
(self.TRANSUP if currLeg == "k" and
nextLeg != "K" else self.REFLECTTOP))
elif isNextLegDepth:
disconBranch = closest_branch_to_depth(tMod, nextLeg)
self.add_to_branch(tMod, self.currBranch, disconBranch,
isPWave, self.TRANSUP)
else:
raise TauModelError(
"Phase not recognized: {} followed by {}".format(
currLeg, nextLeg))
# Now deal with P and S case.
elif currLeg in ("P", "S"):
if nextLeg in ("P", "S", "Pn", "Sn", "END"):
if self.endAction == self.TRANSDOWN or \
self.endAction == self.REFLECTTOP:
# Downgoing, so must first turn in mantle.
self.add_to_branch(tMod, self.currBranch,
tMod.cmbBranch - 1, isPWave,
self.TURN)
self.add_to_branch(tMod, self.currBranch, 0, isPWave,
self.REFLECTTOP)
elif nextLeg[0] == "v":
disconBranch = closest_branch_to_depth(tMod, nextLeg[1])
if self.currBranch <= disconBranch - 1:
self.add_to_branch(tMod, self.currBranch,
disconBranch - 1, isPWave,
self.REFLECTBOT)
else:
raise TauModelError(
"Phase not recognised: {currLeg} followed by "
"{nextLeg} when currBranch > disconBranch".format(
**locals()))
elif nextLeg[0] == "^":
disconBranch = closest_branch_to_depth(tMod, nextLeg[1])
if prevLeg == "K":
self.add_to_branch(tMod, self.currBranch, disconBranch,
isPWave, self.REFLECTTOP)
elif prevLeg[0] == "^" or prevLeg in ("P", "S", "p", "s",
"START"):
self.add_to_branch(
tMod, self.currBranch, tMod.cmbBranch - 1, isPWave,
self.TURN)
self.add_to_branch(tMod, self.currBranch, disconBranch,
isPWave, self.REFLECTTOP)
elif ((prevLeg[0] == "v" and
disconBranch < closest_branch_to_depth(
tMod, prevLeg[1]) or
(prevLeg == "m" and
disconBranch < tMod.mohoBranch) or
(prevLeg == "c" and
disconBranch < tMod.cmbBranch))):
self.add_to_branch(tMod, self.currBranch, disconBranch,
isPWave, self.REFLECTTOP)
else:
raise TauModelError(
"Phase not recognised: {currLeg} followed by "
"{nextLeg} when currBranch > disconBranch".format(
**locals()))
elif nextLeg == "c":
self.add_to_branch(tMod, self.currBranch,
tMod.cmbBranch - 1, isPWave,
self.REFLECTBOT)
elif nextLeg == "K":
self.add_to_branch(tMod, self.currBranch,
tMod.cmbBranch - 1, isPWave,
self.TRANSDOWN)
elif nextLeg == "m" or (isNextLegDepth and
nextLegDepth < tMod.cmbDepth):
# Treat the Moho in the same way as 410 type
# discontinuities.
disconBranch = closest_branch_to_depth(tMod, nextLeg)
if self.endAction == self.TURN \
or self.endAction == self.REFLECTBOT \
or self.endAction == self.TRANSUP:
# Upgoing section
if disconBranch > self.currBranch:
# Check the discontinuity below the current
# branch when the ray should be upgoing
raise TauModelError(
"Phase not recognised: {currLeg} followed by "
"{nextLeg} when currBranch > disconBranch"
.format(**locals()))
self.add_to_branch(tMod, self.currBranch, disconBranch,
isPWave, self.TRANSUP)
else:
# Downgoing section, must look at leg after next to
# determine whether to convert on the downgoing or
# upgoing part of the path.
nextnextLeg = self.legs[legNum + 2]
if nextnextLeg == "p" or nextnextLeg == "s":
# Convert on upgoing section
self.add_to_branch(tMod, self.currBranch,
tMod.cmbBranch - 1, isPWave,
self.TURN)
self.add_to_branch(tMod, self.currBranch,
disconBranch, isPWave,
self.TRANSUP)
elif nextnextLeg == "P" or nextnextLeg == "S":
if disconBranch > self.currBranch:
# discon is below current loc
self.add_to_branch(tMod, self.currBranch,
disconBranch - 1, isPWave,
self.TRANSDOWN)
else:
# Discontinuity is above current location,
# but we have a downgoing ray, so this is an
# illegal ray for this source depth.
self.maxRayParam = -1
return
else:
raise TauModelError(
"Phase not recognized: {} followed by {} "
"followed by {}".format(currLeg, nextLeg,
nextnextLeg))
else:
raise TauModelError(
"Phase not recognized: {} followed by {}".format(
currLeg, nextLeg))
elif currLeg[0] in "PS":
if currLeg == "Pdiff" or currLeg == "Sdiff":
# In the diffracted case we trick addtoBranch into
# thinking we are turning, but then make maxRayParam
# equal to minRayParam, which is the deepest turning ray.
if (self.maxRayParam >= tMod.getTauBranch(
tMod.cmbBranch - 1, isPWave).minTurnRayParam >=
self.minRayParam):
self.add_to_branch(tMod, self.currBranch,
tMod.cmbBranch - 1, isPWave,
self.TURN)
self.maxRayParam = self.minRayParam
if nextLeg == "END" or nextLeg[0] in "PS":
self.add_to_branch(tMod, self.currBranch, 0,
isPWave, self.REFLECTTOP)
else:
# Can't have head wave as ray param is not within
# range.
self.maxRayParam = -1
return
elif currLeg in ("Pg", "Sg", "Pn", "Sn"):
if self.currBranch >= tMod.mohoBranch:
# Pg, Pn, Sg and Sn must be above the moho and so is
# not valid for rays coming upwards from below,
# possibly due to the source depth. Setting
# maxRayParam = -1 effectively disallows this phase.
self.maxRayParam = -1
return
if currLeg in ("Pg", "Sg"):
self.add_to_branch(tMod, self.currBranch,
tMod.mohoBranch - 1, isPWave,
self.TURN)
self.add_to_branch(tMod, self.currBranch, 0, isPWave,
self.REFLECTTOP)
elif currLeg in ("Pn", "Sn"):
# In the diffracted case we trick addtoBranch into
# thinking we are turning below the Moho, but then
# make the minRayParam equal to maxRayParam,
# which is the head wave ray.
if (self.maxRayParam >= tMod.getTauBranch(
tMod.mohoBranch, isPWave).maxRayParam >=
self.minRayParam):
self.add_to_branch(tMod, self.currBranch,
tMod.mohoBranch, isPWave,
self.TURN)
self.add_to_branch(tMod, self.currBranch,
tMod.mohoBranch, isPWave,
self.TRANSUP)
self.minRayParam = self.maxRayParam
if nextLeg == "END" or nextLeg[0] in "PS":
self.add_to_branch(tMod, self.currBranch, 0,
isPWave, self.REFLECTTOP)
else:
# Can't have head wave as ray param is not
# within range.
self.maxRayParam = -1
return
else:
raise TauModelError(
"Phase not recognized: {} followed by {}".format(
currLeg, nextLeg))
elif currLeg == "K":
if nextLeg in ("P", "S"):
if prevLeg in ("P", "S", "K", "k", "START"):
self.add_to_branch(tMod, self.currBranch,
tMod.iocbBranch - 1, isPWave,
self.TURN)
self.add_to_branch(tMod, self.currBranch, tMod.cmbBranch,
isPWave, self.TRANSUP)
elif nextLeg == "K":
if prevLeg in ("P", "S", "K"):
self.add_to_branch(tMod, self.currBranch,
tMod.iocbBranch - 1, isPWave,
self.TURN)
self.add_to_branch(tMod, self.currBranch, tMod.cmbBranch,
isPWave, self.REFLECTTOP)
elif nextLeg in ("I", "J"):
self.add_to_branch(tMod, self.currBranch,
tMod.iocbBranch - 1, isPWave,
self.TRANSDOWN)
elif nextLeg == "i":
self.add_to_branch(tMod, self.currBranch,
tMod.iocbBranch - 1, isPWave,
self.REFLECTBOT)
else:
raise TauModelError(
"Phase not recognized: {} followed by {}".format(
currLeg, nextLeg))
elif currLeg in ("I", "J"):
self.add_to_branch(tMod, self.currBranch,
tMod.tauBranches.shape[1] - 1, isPWave,
self.TURN)
if nextLeg in ("I", "J"):
self.add_to_branch(tMod, self.currBranch, tMod.iocbBranch,
isPWave, self.REFLECTTOP)
elif nextLeg == "K":
self.add_to_branch(tMod, self.currBranch, tMod.iocbBranch,
isPWave, self.TRANSUP)
elif (currLeg in ("m", "c", "i") or
currLeg[0] in "^v" or isLegDepth):
pass
else:
raise TauModelError(
"Phase not recognized: {} followed by {}".format(currLeg,
nextLeg))
[docs] def phase_conversion(self, tMod, fromBranch, endAction, isPtoS):
"""
Change maxRayParam and minRayParam where there is a phase conversion.
For instance, SKP needs to change the maxRayParam because there are SKS
ray parameters that cannot propagate from the CMB into the mantle as a
P wave.
"""
if endAction == self.TURN:
# Can't phase convert for just a turn point
raise TauModelError("Bad endAction: phase conversion is not "
"allowed at turn points.")
elif endAction == self.REFLECTTOP:
self.maxRayParam = \
min(self.maxRayParam,
tMod.getTauBranch(fromBranch, isPtoS).maxRayParam,
tMod.getTauBranch(fromBranch, not isPtoS).maxRayParam)
elif endAction == self.REFLECTBOT:
self.maxRayParam = min(
self.maxRayParam,
tMod.getTauBranch(fromBranch, isPtoS).minTurnRayParam,
tMod.getTauBranch(fromBranch, not isPtoS).minTurnRayParam)
elif endAction == self.TRANSUP:
self.maxRayParam = min(
self.maxRayParam,
tMod.getTauBranch(fromBranch, isPtoS).maxRayParam,
tMod.getTauBranch(fromBranch - 1, not isPtoS).minTurnRayParam)
elif endAction == self.TRANSDOWN:
self.maxRayParam = min(
self.maxRayParam,
tMod.getTauBranch(fromBranch, isPtoS).minRayParam,
tMod.getTauBranch(fromBranch + 1, not isPtoS).maxRayParam)
else:
raise TauModelError("Illegal endAction = {}".format(endAction))
[docs] def add_to_branch(self, tMod, startBranch, endBranch, isPWave, endAction):
"""
Add branch numbers to branchSeq.
Branches from startBranch to endBranch, inclusive, are added in order.
Also, currBranch is set correctly based on the value of endAction.
endAction can be one of TRANSUP, TRANSDOWN, REFLECTTOP, REFLECTBOT, or
TURN.
"""
self.endAction = endAction
if endAction == self.TURN:
endOffset = 0
isDownGoing = True
self.minRayParam = max(
self.minRayParam,
tMod.getTauBranch(endBranch, isPWave).minTurnRayParam)
elif endAction == self.REFLECTTOP:
endOffset = 0
isDownGoing = False
self.maxRayParam = min(
self.maxRayParam,
tMod.getTauBranch(endBranch, isPWave).maxRayParam)
elif endAction == self.REFLECTBOT:
endOffset = 0
isDownGoing = True
self.maxRayParam = min(
self.maxRayParam,
tMod.getTauBranch(endBranch, isPWave).minTurnRayParam)
elif endAction == self.TRANSUP:
endOffset = -1
isDownGoing = False
self.maxRayParam = min(
self.maxRayParam,
tMod.getTauBranch(endBranch, isPWave).maxRayParam)
elif endAction == self.TRANSDOWN:
endOffset = 1
isDownGoing = True
self.maxRayParam = min(
self.maxRayParam,
tMod.getTauBranch(endBranch, isPWave).minRayParam)
else:
raise TauModelError("Illegal endAction: {}".format(endAction))
if isDownGoing:
# Must be downgoing, so increment i.
for i in range(startBranch, endBranch + 1):
self.branchSeq.append(i)
self.downGoing.append(isDownGoing)
self.waveType.append(isPWave)
else:
# Upgoing, so decrement i.
for i in range(startBranch, endBranch - 1, -1):
self.branchSeq.append(i)
self.downGoing.append(isDownGoing)
self.waveType.append(isPWave)
self.currBranch = endBranch + endOffset
[docs] def sum_branches(self, tMod):
"""Sum the appropriate branches for this phase."""
if self.maxRayParam < 0 or self.minRayParam > self.maxRayParam:
# Phase has no arrivals, possibly due to source depth.
self.ray_param = np.empty(0)
self.minRayParam = -1
self.maxRayParam = -1
self.dist = np.empty(0)
self.time = np.empty(0)
self.maxDistance = -1
return
# Special case for surface waves.
if self.name.endswith("kmps"):
self.dist = np.zeros(2)
self.time = np.zeros(2)
self.ray_param = np.empty(2)
self.ray_param[0] = tMod.radiusOfEarth / float(self.name[:-4])
self.dist[1] = 2 * math.pi
self.time[1] = \
2 * math.pi * tMod.radiusOfEarth / float(self.name[:-4])
self.ray_param[1] = self.ray_param[0]
self.minDistance = 0
self.maxDistance = 2 * math.pi
self.downGoing.append(True)
return
# Find the ray parameter index that corresponds to the minRayParam
# and maxRayParam.
index = np.where(tMod.ray_params >= self.minRayParam)[0]
if len(index):
self.minRayParamIndex = index[-1]
index = np.where(tMod.ray_params >= self.maxRayParam)[0]
if len(index):
self.maxRayParamIndex = index[-1]
if self.maxRayParamIndex == 0 \
and self.minRayParamIndex == len(tMod.ray_params) - 1:
# All ray parameters are valid so just copy:
self.ray_param = tMod.ray_param.copy()
elif self.maxRayParamIndex == self.minRayParamIndex:
# if "Sdiff" in self.name or "Pdiff" in self.name:
# self.ray_param = [self.minRayParam, self.minRayParam]
# elif "Pn" in self.name or "Sn" in self.name:
# self.ray_param = [self.minRayParam, self.minRayParam]
if self.name.endswith("kmps"):
self.ray_param = np.array([0, self.maxRayParam])
else:
self.ray_param = np.array([self.minRayParam, self.minRayParam])
else:
# Only a subset of the ray parameters is valid so use these.
self.ray_param = \
tMod.ray_params[self.maxRayParamIndex:
self.minRayParamIndex + 1].copy()
self.dist = np.zeros(shape=self.ray_param.shape)
self.time = np.zeros(shape=self.ray_param.shape)
# Initialise the counter for each branch to 0. 0 is P and 1 is S.
timesBranches = np.zeros((2, tMod.tauBranches.shape[1]))
# Count how many times each branch appears in the path.
# waveType is at least as long as branchSeq
for wt, bs in zip(self.waveType, self.branchSeq):
if wt:
timesBranches[0][bs] += 1
else:
timesBranches[1][bs] += 1
# Sum the branches with the appropriate multiplier.
size = self.minRayParamIndex - self.maxRayParamIndex + 1
index = slice(self.maxRayParamIndex, self.minRayParamIndex + 1)
for i in range(tMod.tauBranches.shape[1]):
tb = timesBranches[0, i]
tbs = timesBranches[1, i]
taub = tMod.tauBranches[0, i]
taubs = tMod.tauBranches[1, i]
if tb != 0:
self.dist[:size] += tb * taub.dist[index]
self.time[:size] += tb * taub.time[index]
if tbs != 0:
self.dist[:size] += tbs * taubs.dist[index]
self.time[:size] += tbs * taubs.time[index]
if "Sdiff" in self.name or "Pdiff" in self.name:
if tMod.sMod.depthInHighSlowness(tMod.cmbDepth - 1e-10,
self.minRayParam,
self.name[0] == "P"):
# No diffraction if there is a high slowness zone at the CMB.
self.minRayParam = -1
self.maxRayParam = -1
self.maxDistance = -1
self.time = np.empty(0)
self.dist = np.empty(0)
self.ray_param = np.empty(0)
return
else:
self.dist[1] = self.dist[0] + np.radians(self.maxDiffraction)
self.time[1] = self.time[0] + \
np.radians(self.maxDiffraction) * self.minRayParam
elif "Pn" in self.name or "Sn" in self.name:
self.dist[1] = self.dist[0] + np.radians(self.maxRefraction)
self.time[1] = (self.time[0] +
np.radians(self.maxRefraction) * self.minRayParam)
elif self.maxRayParamIndex == self.minRayParamIndex:
self.dist[1] = self.dist[0]
self.time[1] = self.time[0]
self.minDistance = np.min(self.dist)
self.maxDistance = np.max(self.dist)
# Now check to see if our ray parameter range includes any ray
# parameters that are associated with high slowness zones. If so,
# then we will need to insert a "shadow zone" into our time and
# distance arrays. It is represented by a repeated ray parameter.
for isPwave in [True, False]:
hsz = tMod.sMod.highSlownessLayerDepthsP \
if isPwave \
else tMod.sMod.highSlownessLayerDepthsS
indexOffset = 0
for hszi in hsz:
if self.maxRayParam > hszi.ray_param > self.minRayParam:
# There is a high slowness zone within our ray parameter
# range so might need to add a shadow zone. Need to
# check if the current wave type is part of the phase at
# this depth/ray parameter.
branchNum = tMod.findBranch(hszi.topDepth)
foundOverlap = False
for legNum in range(len(self.branchSeq)):
# Check for downgoing legs that cross the high
# slowness zone with the same wave type.
if (self.branchSeq[legNum] == branchNum and
self.waveType[legNum] == isPwave and
self.downGoing[legNum] is True and
self.branchSeq[legNum - 1] == branchNum - 1 and
self.waveType[legNum - 1] == isPwave and
self.downGoing[legNum - 1] is True):
foundOverlap = True
break
if foundOverlap:
hszIndex = np.where(self.ray_param == hszi.ray_param)
hszIndex = hszIndex[0][0]
newlen = self.ray_param.shape[0] + 1
new_ray_params = np.empty(newlen)
newdist = np.empty(newlen)
newtime = np.empty(newlen)
new_ray_params[:hszIndex] = self.ray_param[:hszIndex]
newdist[:hszIndex] = self.dist[:hszIndex]
newtime[:hszIndex] = self.time[:hszIndex]
# Sum the branches with an appropriate multiplier.
new_ray_params[hszIndex] = hszi.ray_param
newdist[hszIndex] = 0
newtime[hszIndex] = 0
for tb, tbs, taub, taubs in zip(timesBranches[0],
timesBranches[1],
tMod.tauBranches[0],
tMod.tauBranches[1]):
if tb != 0 and taub.topDepth < hszi.topDepth:
newdist[hszIndex] += tb * taub.dist[
self.maxRayParamIndex + hszIndex -
indexOffset]
newtime[hszIndex] += tb * taub.time[
self.maxRayParamIndex + hszIndex -
indexOffset]
if tbs != 0 and taubs.topDepth < hszi.topDepth:
newdist[hszIndex] += tbs * taubs.dist[
self.maxRayParamIndex + hszIndex -
indexOffset]
newtime[hszIndex] += tbs * taubs.time[
self.maxRayParamIndex + hszIndex -
indexOffset]
newdist[hszIndex + 1:] = self.dist[hszIndex:]
newtime[hszIndex + 1:] = self.time[hszIndex:]
new_ray_params[hszIndex + 1:] = \
self.ray_param[hszIndex:]
indexOffset += 1
self.dist = newdist
self.time = newtime
self.ray_param = new_ray_params
[docs] def calc_time(self, degrees):
"""
Calculate arrival times for this phase, sorted by time.
"""
# 10 should be enough. This is only for one phase.
r_dist = np.empty(10, dtype=np.float64)
r_ray_num = np.empty(10, dtype=np.int32)
# This saves around 17% runtime when calculating arrival times which
# is probably the major use case.
phase_count = clibtau.seismic_phase_calc_time_inner_loop(
float(degrees),
self.maxDistance,
self.dist,
self.ray_param,
r_dist,
r_ray_num,
len(self.dist)
)
arrivals = []
for _i in range(phase_count):
arrivals.append(self.linear_interp_arrival(
degrees, r_dist[_i], r_ray_num[_i], self.name, self.puristName,
self.source_depth))
return arrivals
[docs] def calc_pierce(self, degrees):
"""
Calculate pierce points for this phase.
First calculates arrivals, then the "pierce points" corresponding to
the stored arrivals. The pierce points are stored within each arrival
object.
"""
arrivals = self.calc_time(degrees)
for arrival in arrivals:
self.calc_pierce_from_arrival(arrival)
return arrivals
[docs] def calc_pierce_from_arrival(self, currArrival):
"""
Calculate the pierce points for a particular arrival.
The returned arrival is the same as the input argument but now has the
pierce points filled in.
"""
# Find the ray parameter index that corresponds to the arrival ray
# parameter in the TauModel, ie it is between rayNum and rayNum+1,
# We know that it must be <model.ray_param.length-1 since the last
# ray parameter sample is 0 in a spherical model.
rayNum = 0
for i, rp in enumerate(self.tMod.ray_params[:-1]):
if rp >= currArrival.ray_param:
rayNum = i
else:
break
# Here we use ray parameter and dist info stored within the
# SeismicPhase so we can use currArrival.ray_param_index, which
# may not correspond to rayNum (for model.ray_param).
ray_param_a = self.ray_param[currArrival.ray_param_index]
ray_param_b = self.ray_param[currArrival.ray_param_index + 1]
distA = self.dist[currArrival.ray_param_index]
distB = self.dist[currArrival.ray_param_index + 1]
distRatio = (currArrival.purist_dist - distA) / (distB - distA)
distRayParam = distRatio * (ray_param_b - ray_param_a) + ray_param_a
# + 2 for first point and kmps, if it exists.
pierce = np.empty(len(self.branchSeq) + 2, dtype=TimeDist)
# First pierce point is always 0 distance at the source depth.
pierce[0] = (distRayParam, 0, 0, self.tMod.source_depth)
index = 1
branchDist = 0
branchTime = 0
# Loop from 0 but already done 0 [I just copy the comments, sorry!],
# so the pierce point when the ray leaves branch i is stored in i + 1.
# Use linear interpolation between rays that we know.
assert len(self.branchSeq) == len(self.waveType) == len(self.downGoing)
for branchNum, isPWave, isDownGoing in zip(self.branchSeq,
self.waveType,
self.downGoing):
# Save the turning depths for the ray parameter for both P and
# S waves. This way we get the depth correct for any rays that
# turn within a layer. We have to do this on a per branch basis
# because of converted phases, e.g. SKS.
tauBranch = self.tMod.getTauBranch(branchNum, isPWave)
if distRayParam > tauBranch.maxRayParam:
turnDepth = tauBranch.topDepth
elif distRayParam <= tauBranch.minRayParam:
turnDepth = tauBranch.botDepth
else:
if (isPWave or self.tMod.sMod.depthInFluid((
tauBranch.topDepth + tauBranch.botDepth) / 2)):
turnDepth = self.tMod.sMod.findDepth_from_depths(
distRayParam,
tauBranch.topDepth,
tauBranch.botDepth,
True)
else:
turnDepth = self.tMod.sMod.findDepth_from_depths(
distRayParam,
tauBranch.topDepth,
tauBranch.botDepth,
isPWave)
if any(x in self.name for x in ["Pdiff", "Pn", "Sdiff", "Sn"]):
# Head waves and diffracted waves are a special case.
distA = tauBranch.dist[rayNum]
timeA = tauBranch.time[rayNum]
distB, timeB = distA, timeA
else:
distA = tauBranch.dist[rayNum]
timeA = tauBranch.time[rayNum]
distB = tauBranch.dist[rayNum + 1]
timeB = tauBranch.time[rayNum + 1]
branchDist += distRatio * (distB - distA) + distA
prevBranchTime = np.array(branchTime, copy=True)
branchTime += distRatio * (timeB - timeA) + timeA
if isDownGoing:
branchDepth = min(tauBranch.botDepth, turnDepth)
else:
branchDepth = min(tauBranch.topDepth, turnDepth)
# Make sure ray actually propagates in this branch; leave a little
# room for numerical chatter.
if abs(prevBranchTime - branchTime) > 1e-10:
pierce[index] = (distRayParam, branchTime, branchDist,
branchDepth)
index += 1
if any(x in self.name for x in ["Pdiff", "Pn", "Sdiff", "Sn"]):
pierce, index = self.handle_special_waves(currArrival,
pierce,
index)
elif "kmps" in self.name:
pierce[index] = (distRayParam, currArrival.time,
currArrival.purist_dist, 0)
index += 1
currArrival.pierce = pierce[:index]
# The arrival is modified in place and must (?) thus be returned.
return currArrival
[docs] def calc_path(self, degrees):
"""
Calculate the paths this phase takes through the Earth model.
Only calls :meth:`calc_path_from_arrival`.
"""
arrivals = self.calc_time(degrees)
for arrival in arrivals:
self.calc_path_from_arrival(arrival)
return arrivals
[docs] def calc_path_from_arrival(self, currArrival):
"""
Calculate the paths this phase takes through the Earth model.
"""
# Find the ray parameter index that corresponds to the arrival ray
# parameter in the TauModel, i.e. it is between rayNum and rayNum + 1.
tempTimeDist = np.array([(currArrival.ray_param, 0, 0,
self.tMod.source_depth)], dtype=TimeDist)
# pathList is a list of lists.
pathList = [tempTimeDist]
for i, branchNum, isPWave, isDownGoing in zip(count(), self.branchSeq,
self.waveType,
self.downGoing):
tempTimeDist = self.tMod.getTauBranch(branchNum, isPWave)\
.path(currArrival.ray_param, isDownGoing, self.tMod.sMod)
if len(tempTimeDist):
pathList.append(tempTimeDist)
if np.any(tempTimeDist['dist'] < 0):
raise RuntimeError("Path is backtracking, "
"this is impossible.")
# Special case for head and diffracted waves:
if(branchNum == self.tMod.cmbBranch - 1 and
i < len(self.branchSeq) - 1 and
self.branchSeq[i + 1] == self.tMod.cmbBranch - 1 and
("Pdiff" in self.name or "Sdiff" in self.name)):
dist_diff = currArrival.purist_dist - self.dist[0]
diffTD = np.array([(
currArrival.ray_param,
dist_diff * currArrival.ray_param,
dist_diff,
self.tMod.cmbDepth)], dtype=TimeDist)
pathList.append(diffTD)
elif(branchNum == self.tMod.mohoBranch - 1 and
i < len(self.branchSeq) - 1 and
self.branchSeq[i + 1] == self.tMod.mohoBranch - 1 and
("Pn" in self.name or "Sn" in self.name)):
# Can't have both Pn and Sn in a wave, so one of these is 0.
numFound = max(self.name.count("Pn"), self.name.count("Sn"))
dist_head = (currArrival.purist_dist - self.dist[0]) / numFound
headTD = np.array([(
currArrival.ray_param,
dist_head * currArrival.ray_param,
dist_head,
self.tMod.mohoDepth)], dtype=TimeDist)
pathList.append(headTD)
if "kmps" in self.name:
# kmps phases have no branches, so need to end them at the arrival
# distance.
headTD = np.array([(
currArrival.ray_param,
currArrival.purist_dist * currArrival.ray_param,
currArrival.purist_dist,
0)], dtype=TimeDist)
pathList.append(headTD)
currArrival.path = np.concatenate(pathList)
np.cumsum(currArrival.path['time'], out=currArrival.path['time'])
np.cumsum(currArrival.path['dist'], out=currArrival.path['dist'])
return currArrival
[docs] def handle_special_waves(self, currArrival, pierce, index):
"""
Handle head or diffracted waves.
It is assumed that a phase can be a diffracted wave or a head wave, but
not both. Nor can it be a head wave or diffracted wave for both P and
S.
"""
for ps in ["Pn", "Sn", "Pdiff", "Sdiff"]:
if ps in self.name:
phaseSeg = ps
break
else:
raise TauModelError("No head/diff segment in" + str(self.name))
if phaseSeg in ["Pn", "Sn"]:
headDepth = self.tMod.mohoDepth
else:
headDepth = self.tMod.cmbDepth
numFound = self.name.count(phaseSeg)
refractDist = currArrival.purist_dist - self.dist[0]
refractTime = refractDist * currArrival.ray_param
# This is a little weird as we are not checking where we are in
# the phase name, but simply if the depth matches. This likely
# works in most cases, but may not for head/diffracted waves that
# undergo a phase change, if that type of phase can even exist.
mask = pierce['depth'][:index] == headDepth
adjust = np.cumsum(mask)
pierce['time'][:index] += adjust * refractTime / numFound
pierce['dist'][:index] += adjust * refractDist / numFound
head_index = np.where(mask)[0]
if len(head_index):
head_index += 1
td = pierce[head_index]
pierce = np.insert(pierce, head_index, td)
index += len(head_index)
return pierce, index
[docs] def linear_interp_arrival(self, degrees, searchDist, rayNum, name,
puristName, source_depth):
arrivalTime = ((searchDist - self.dist[rayNum]) /
(self.dist[rayNum + 1] - self.dist[rayNum]) *
(self.time[rayNum + 1] - self.time[rayNum]) +
self.time[rayNum])
arrivalRayParam = ((searchDist - self.dist[rayNum + 1]) *
(self.ray_param[rayNum] -
self.ray_param[rayNum + 1]) /
(self.dist[rayNum] - self.dist[rayNum + 1]) +
self.ray_param[rayNum + 1])
if name.endswith("kmps"):
takeoffAngle = 0
incidentAngle = 0
else:
vMod = self.tMod.sMod.vMod
if self.downGoing[0]:
takeoffVelocity = vMod.evaluateBelow(source_depth, name[0])
else:
# Fake negative velocity so angle is negative in case of
# upgoing ray.
takeoffVelocity = -1 * vMod.evaluateAbove(source_depth,
name[0])
takeoffAngle = np.degrees(math.asin(np.clip(
takeoffVelocity * arrivalRayParam /
(self.tMod.radiusOfEarth - self.source_depth), -1.0, 1.0)))
lastLeg = self.legs[-2][0] # very last item is "END"
incidentAngle = np.degrees(math.asin(
vMod.evaluateBelow(0, lastLeg) * arrivalRayParam /
self.tMod.radiusOfEarth))
return Arrival(self, degrees, arrivalTime, searchDist, arrivalRayParam,
rayNum, name, puristName, source_depth, takeoffAngle,
incidentAngle)
@classmethod
[docs] def get_earliest_arrival(cls, relPhases, degrees):
raise NotImplementedError("baaa")
[docs]def closest_branch_to_depth(tMod, depthString):
"""
Find the closest discontinuity to the given depth that can have
reflections and phase transformations.
"""
if depthString == "m":
return tMod.mohoBranch
elif depthString == "c":
return tMod.cmbBranch
elif depthString == "i":
return tMod.iocbBranch
# Non-standard boundary, given by a number: must look for it.
disconBranch = -1
disconMax = 1e300
disconDepth = float(depthString)
for i, tBranch in enumerate(tMod.tauBranches[0]):
if (abs(disconDepth - tBranch.topDepth) < disconMax and not
any(ndc == tBranch.topDepth for ndc in tMod.noDisconDepths)):
disconBranch = i
disconMax = abs(disconDepth - tBranch.topDepth)
return disconBranch
[docs]def self_tokenizer(scanner, token):
return token
[docs]def wrong_phase(scanner, token):
raise ValueError("Invalid phase name: %s cannot be followed by %s in %s" %
(token[0], token[1], token))
tokenizer = re.Scanner([
# Surface wave phase velocity "phases"
(r"\.?\d+\.?\d*kmps", self_tokenizer),
# Composite legs.
(r"Pn|Sn|Pg|Sg|Pb|Sb|Pdiff|Sdiff", self_tokenizer),
# Reflections.
(r"([\^v])([mci]|\.?\d+\.?\d*)", self_tokenizer),
# Invalid phases.
(r"[PS][ps]", wrong_phase),
# Single legs.
(r"[KkIiJmcPpSs]", self_tokenizer),
# Single numerical value
(r"\.?\d+\.?\d*", self_tokenizer)
])
[docs]def leg_puller(name):
"""
Tokenize a phase name into legs.
For example, ``PcS`` becomes ``'P' + 'c' + 'S'`` while ``p^410P`` would
become ``'p' + '^410' + 'P'``. Once a phase name has been broken into
tokens, we can begin to construct the sequence of branches to which it
corresponds. Only minor error checking is done at this point, for instance
``PIP`` generates an exception but ``^410`` doesn't. It also appends
``"END"`` as the last leg.
"""
results, remainder = tokenizer.scan(name)
if remainder:
raise ValueError("Invalid phase name: %s could not be parsed in %s"
% (str(remainder), name))
return results + ["END"]