Repository: obspy/obspy

Issue:
Error in TauPy Model: The Phase "ScScS"
I noticed that the taup routines work fine for the phase "ScScS" (i.e., NOT ScSScS). See below. The taup_path for ScScS is also strange. Philip Crotwell's TauP package has the same issue and I reported the bug to Philip as well. Perhaps there is something wrong in the phase parser?

>>> from obspy.taup import TauPyModel
>>> scs_times  = TauPyModel( model="ak135" ).get_travel_times( 0, 0, phase_list=["ScS","ScScS","ScSScS"] )
>>> print (scs_times)
3 arrivals
	ScS phase arrival at 935.771 seconds
	ScScS phase arrival at 1265.670 seconds
	ScSScS phase arrival at 1871.542 seconds

Cheers,
Jeroen

<!--

**GitHub should mainly be used for bug reports and code developments/discussions. For generic questions, please use either the [[obspy-users] mailing list](http://lists.swapbytes.de/mailman/listinfo/obspy-users) (for scientific questions to the general ObsPy community) or our [gitter chat](https://gitter.im/obspy/obspy) (for technical questions and direct contact to the developers).**

Before submitting an Issue, please review the [Issue Guidelines](https://github.com/obspy/obspy/blob/master/CONTRIBUTING.md#submitting-an-issue).

-->

* Please check whether the bug was already reported or fixed.
* Please provide the following information:
  -  ObsPy version, Python version and Platform (Windows, OSX, Linux ...)
  -  How did you install ObsPy and Python (pip, anaconda, from source ...)
  -  If possible please supply a [short, self contained, correct example](http://sscce.org/) that
     demonstrates the issue.
  -  If this is a regression (used to work in an earlier version of ObsPy),
     please note when it used to work.
* Please take the time to [format your ticket appropriately](https://guides.github.com/features/mastering-markdown/)
  (e.g. put error tracebacks into code blocks)



Discussion:

Resolving patch:
diff --git a/obspy/taup/seismic_phase.py b/obspy/taup/seismic_phase.py
--- a/obspy/taup/seismic_phase.py
+++ b/obspy/taup/seismic_phase.py
@@ -117,6 +117,8 @@ def __init__(self, name, tau_model, receiver_depth=0.0):
         # List of branch_seq positions where a head or diffracted
         # segment occurs.
         self.head_or_diffract_seq = []
+        # Description of segments of the phase.
+        self.segment_list = []
         # 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().
@@ -936,6 +938,14 @@ def add_to_branch(self, tau_model, start_branch, end_branch, is_p_wave,
         else:
             raise TauModelError("Illegal end_action: {}".format(end_action))
 
+        segment = SeismicPhaseSegment(self.tau_model, start_branch, end_branch,
+                                      is_p_wave, end_action, is_down_going)
+        if self.segment_list:
+            # Perform some sanity checks on the segment being added
+            prev_segment = self.segment_list[-1]
+            _check_segment_consistency(prev_segment, segment)
+        self.segment_list.append(segment)
+
         if is_down_going:
             if start_branch > end_branch:
                 # Can't be downgoing as we are already below.
@@ -1697,3 +1707,67 @@ def leg_puller(name):
         raise ValueError("Invalid phase name: %s could not be parsed in %s"
                          % (str(remainder), name))
     return results + ["END"]
+
+
+class SeismicPhaseSegment(object):
+    def __init__(self, tau_model, start_branch, end_branch, is_p_wave,
+                 end_action, is_down_going):
+        self.tau_model = tau_model
+        self.start_branch = start_branch
+        self.end_branch = end_branch
+        self.is_p_wave = is_p_wave
+        self.end_action = end_action
+        self.is_down_going = is_down_going
+
+
+def _check_segment_consistency(prev_segment, segment):
+    """
+    Sanity checks to see if the SeismicPhaseSegment
+    being added is consistent with the previous segment.
+    Raises a TauModelError is there is a problem.
+    """
+    if segment.is_down_going:
+        if prev_segment.end_branch > segment.start_branch:
+            raise TauModelError(
+                f"Segment is downgoing, but we are already below the "
+                f"start: {segment.end_action}")
+        if prev_segment.end_action == _ACTIONS["reflect_topside"]:
+            raise TauModelError(
+                f"Segment is downgoing, but previous action was to "
+                f"reflect up: {segment.end_action}")
+        if prev_segment.end_action == _ACTIONS["turn"]:
+            raise TauModelError(
+                f"Segment is downgoing, but previous action was to "
+                f"turn: {segment.end_action}")
+        if prev_segment.end_action == _ACTIONS["transup"]:
+            raise TauModelError(
+                f"Segment is downgoing, but previous action was to "
+                f"transmit up: {segment.end_action}")
+        if (prev_segment.end_branch == segment.start_branch
+                and not prev_segment.is_down_going and
+                prev_segment.end_action !=
+                _ACTIONS["reflect_underside"]):
+            raise TauModelError(
+                f"Segment is downgoing, but previous action was not to"
+                f"reflect underside: {segment.end_action}")
+    else:
+        if prev_segment.end_branch < segment.start_branch:
+            raise TauModelError(
+                f"Segment is upgoing, but we are already above the "
+                f"start: {segment.end_action}")
+        if prev_segment.end_action == _ACTIONS["reflect_underside"]:
+            raise TauModelError(
+                f"Segment is upgoing, but previous action was to "
+                f"underside reflect down:  {segment.end_action}")
+        if prev_segment.end_action == _ACTIONS["transdown"]:
+            raise TauModelError(
+                f"Segment is upgoing, but previous action was to "
+                f"trans down: {segment.end_action}")
+        if (prev_segment.end_branch == segment.start_branch
+                and prev_segment.is_down_going and
+                prev_segment.end_action not in
+                (_ACTIONS["turn"], _ACTIONS["diffract"],
+                    _ACTIONS["reflect_topside"])):
+            raise TauModelError(
+                f"Segment is upgoing, but previous action was not to "
+                f"reflect topside: {segment.end_action}")


Test patch:
diff --git a/obspy/taup/tests/test_seismic_phase.py b/obspy/taup/tests/test_seismic_phase.py
--- a/obspy/taup/tests/test_seismic_phase.py
+++ b/obspy/taup/tests/test_seismic_phase.py
@@ -13,7 +13,7 @@
 from obspy.taup.tau_model import TauModel
 from obspy.taup.seismic_phase import SeismicPhase
 from obspy.taup.taup_create import build_taup_model
-
+from obspy.taup.helper_classes import TauModelError
 
 # Most generic way to get the data folder path.
 DATA = os.path.join(os.path.dirname(os.path.abspath(
@@ -100,3 +100,17 @@ def test_diffracted_phases(self):
             assert abs(arrival.time - time) < tol
             assert abs(arrival.pierce["time"][-1] - time) < tol
             assert abs(arrival.path["time"][-1] - time) < tol
+
+    def test_phase_names(self, tau_model):
+        """
+        Simple check to see if illegal phase names are caught.
+        """
+        legal_phase_names = ["ScS", "ScSScS"]
+        illegal_phase_names = ["ScScS", "PKIKPKIKP", "PKIKIKP"]
+
+        for name in legal_phase_names:
+            SeismicPhase(name, tau_model)
+
+        for name in illegal_phase_names:
+            with pytest.raises(TauModelError):
+                SeismicPhase(name, tau_model)