Repository: obspy/obspy

Issue:
Catalog to NLLOC_OBS: picks without time_error
When converting a `Catalog` object to `NLLOC_OBS`, phase picks without a time error are given an error of `-1` (see [here](https://github.com/obspy/obspy/blob/master/obspy/io/nlloc/core.py#L409)). 

I did some check, and I think that NonLinLoc interprets the time error always as positive, which gives a time error of one second! (pinging @alomax to be sure of that).

So, I think that the time error should be set to zero in these cases.


Discussion:
Correct.  The time error is always squared on use, so a value of -1 will be used as 1sec.
If no pick uncertainty is available, then 0.0 should be used. But it is undesirable not to have pick uncertainties in NLL, as the probabilistic location search relies on pick and travel-time uncertainties for weighting the misfit function.  If pick uncertainty is 0.0, then the travel-time uncertainties have to be finite and carefully set (LOCGAU or LOCGAU2).
The pick uncertainty can also be specified with type QUAL <int> instead of GAU <float>, in which case a LOCQUAL2ERR statement in the NLLoc control file sets the time uncertainty based on the <int> value.  So when a pick uncertainty is not available, setting QUAL 0, for example, would allow assigning a pick uncertainty at NLLoc run time.  But this would break at NLLoc runtime if a LOCQUAL2ERR statement is not used...
Ok, @claudiodsf so I guess we can only set it to `0.0` if we have no info then? If that'd be the case @LMurrayBergquist can make the change..
So, if I understood correctly, a NLLOC_OBS line can be something like:
```
UH1    ?    ?    ? P      ? 20100527 1656 26.1300 QUAL 0 -1.00e+00 -1.00e+00 -1.00e+00
```
This would force the user to take action and define what a `QUAL 0` pick means in NonLinLoc.

Isn't that a better option?
The QUAL system follows the old hypo* location code 0-4 or 0-N weighting system.  So QUAL 0 would be the best quality / smallest uncertainty pick.  But if only QUAL 0 is present, then, yes, this forces a definition of the uncertainty.  Currently, NLLoc will probably crash if there is no LOCQUAL2ERR statement and QUAL is used, so not a very elegant solution...
So, from your perspective, it is better to put `GAU 0.0`? (We need to put something, anyway)
That is probably the safest compromise. Hopefully most pick readings moving through QuakeML and ObsPy will have uncertainties...
👍 So will @LMurrayBergquist make the change? (otherwise I can do it 😉 )
Sure! If you haven't changed it, I can do that now. 

Resolving patch:
diff --git a/obspy/io/nlloc/core.py b/obspy/io/nlloc/core.py
--- a/obspy/io/nlloc/core.py
+++ b/obspy/io/nlloc/core.py
@@ -334,14 +334,20 @@ def _read_single_hypocenter(lines, coordinate_converter, original_picks,
         arrival.phase = phase
         if nlloc_file_format_version == 1:
             arrival.distance = kilometer2degrees(float(line[21]))
-            arrival.azimuth = float(line[23])
-            arrival.takeoff_angle = float(line[24])
+            arrival.azimuth = float(line[22])
+            # do not read in take off angle, if the ray takeoff quality is
+            # given as "0" for "unreliable", see #3224
+            if int(line[25]) != 0:
+                arrival.takeoff_angle = float(line[24])
             arrival.time_residual = float(line[16])
             arrival.time_weight = float(line[17])
         elif nlloc_file_format_version == 2:
             arrival.distance = kilometer2degrees(float(line[22]))
-            arrival.azimuth = float(line[24])
-            arrival.takeoff_angle = float(line[25])
+            arrival.azimuth = float(line[23])
+            # do not read in take off angle, if the ray takeoff quality is
+            # given as "0" for "unreliable", see #3224
+            if int(line[26]) != 0:
+                arrival.takeoff_angle = float(line[25])
             arrival.time_residual = float(line[17])
             arrival.time_weight = float(line[18])
         else:
@@ -432,13 +438,18 @@ def write_nlloc_obs(catalog, filename, **kwargs):
         date = pick.time.strftime("%Y%m%d")
         hourminute = pick.time.strftime("%H%M")
         seconds = pick.time.second + pick.time.microsecond * 1e-6
-        time_error = pick.time_errors.uncertainty or -1
-        if time_error == -1:
-            try:
-                time_error = (pick.time_errors.upper_uncertainty +
-                              pick.time_errors.lower_uncertainty) / 2.0
-            except Exception:
-                pass
+        if pick.time_errors.upper_uncertainty is not None and \
+                pick.time_errors.lower_uncertainty is not None:
+            time_error = (pick.time_errors.upper_uncertainty +
+                          pick.time_errors.lower_uncertainty) / 2.0
+        elif pick.time_errors.uncertainty is not None:
+            time_error = pick.time_errors.uncertainty
+        else:
+            # see discussion in #2371
+            msg = ("Writing pick without time uncertainty. Time uncertainty "
+                   "will be written as '0.0'")
+            warnings.warn(msg)
+            time_error = 0.0
         info_ = fmt % (station.ljust(6), "?".ljust(4), component.ljust(4),
                        onset.ljust(1), phase_type.ljust(6), polarity.ljust(1),
                        date, hourminute, seconds, time_error, -1, -1, -1)
diff --git a/obspy/scripts/_print.py b/obspy/scripts/_print.py
deleted file mode 100644
--- a/obspy/scripts/_print.py
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-Print stream information for waveform data in local files.
-"""
-from .print import main
-
-main = main
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -132,7 +132,7 @@
         'obspy-flinn-engdahl = obspy.scripts.flinnengdahl:main',
         'obspy-runtests = obspy.scripts.runtests:main',
         'obspy-reftek-rescue = obspy.scripts.reftekrescue:main',
-        'obspy-print = obspy.scripts._print:main',
+        'obspy-print = obspy.scripts.print:main',
         'obspy-sds-report = obspy.scripts.sds_html_report:main',
         'obspy-scan = obspy.imaging.scripts.scan:main',
         'obspy-plot = obspy.imaging.scripts.plot:main',


Test patch:
diff --git a/obspy/clients/fdsn/tests/test_client.py b/obspy/clients/fdsn/tests/test_client.py
--- a/obspy/clients/fdsn/tests/test_client.py
+++ b/obspy/clients/fdsn/tests/test_client.py
@@ -412,8 +412,9 @@ def test_iris_event_catalog_availability(self):
         """
         Tests the parsing of the available event catalogs.
         """
-        self.assertEqual(set(self.client.services["available_event_catalogs"]),
-                         set(("GCMT", "ISC", "NEIC PDE")))
+        stripped_set = {c.strip() for c in
+                        self.client.services['available_event_catalogs']}
+        assert stripped_set == set(("GCMT", "ISC", "NEIC PDE"))
 
     def test_iris_event_contributors_availability(self):
         """
@@ -861,13 +862,13 @@ def test_help_function_with_iris(self):
             # allow for changes in version number..
             got = normalize_version_number(got)
             expected = normalize_version_number(expected)
-            # catalogs/contributors are checked in separate tests
-            self.assertTrue(got[-2].startswith('Available catalogs:'))
-            self.assertTrue(got[-1].startswith('Available contributors:'))
-            got = got[:-2]
+            # convert newlines to ' '
+            got = ' '.join(got)
+            assert 'Available catalogs:' in got
+            assert 'Available contributors:' in got
             expected = expected[:-2]
-            for line_got, line_expected in zip(got, expected):
-                self.assertEqual(line_got, line_expected)
+            expected = ' '.join(expected)
+            assert got.startswith(expected)
 
             # Reset. Creating a new one is faster then clearing the old one.
             tmp = io.StringIO()
diff --git a/obspy/clients/fdsn/tests/test_eidaws_routing_client.py b/obspy/clients/fdsn/tests/test_eidaws_routing_client.py
--- a/obspy/clients/fdsn/tests/test_eidaws_routing_client.py
+++ b/obspy/clients/fdsn/tests/test_eidaws_routing_client.py
@@ -226,8 +226,7 @@ def test_get_waveforms_bulk(self):
                           str(obspy.UTCDateTime(2017, 1, 2))[:-1]])
         # Everything should be passed on.
         self.assertEqual(p3.call_args[1], {
-            "format": "text", "level": "channel", "longestonly": True,
-            "minimumlength": 2})
+            "level": "channel", "longestonly": True, "minimumlength": 2})
 
     def test_get_stations(self):
         # Some mock routing response.
diff --git a/obspy/io/nlloc/tests/data/nlloc.qml b/obspy/io/nlloc/tests/data/nlloc.qml
--- a/obspy/io/nlloc/tests/data/nlloc.qml
+++ b/obspy/io/nlloc/tests/data/nlloc.qml
@@ -61,55 +61,40 @@ STATISTICS ExpectX -1.32658 Y -0.0487098 Z 3.12781  CovXX 1.21008 XY 0.238028 XZ
         <arrival publicID="smi:local/4822394a-8dba-4b4d-b8d6-47c75d94a600">
           <pickID>smi:local/80f620bf-5550-4fc5-b1a6-5d4394795878</pickID>
           <phase>P</phase>
-          <azimuth>359.0</azimuth>
+          <azimuth>109.48</azimuth>
           <distance>0.00329961097212</distance>
-          <takeoffAngle>
-            <value>-1.0</value>
-          </takeoffAngle>
           <timeResidual>-0.0076</timeResidual>
           <timeWeight>0.9958</timeWeight>
         </arrival>
         <arrival publicID="smi:local/0217b7ba-a9f7-46c8-b9c9-1c3de497a965">
           <pickID>smi:local/804b43a8-fe67-4041-af14-be0a2ea3e493</pickID>
           <phase>P</phase>
-          <azimuth>359.0</azimuth>
+          <azimuth>13.71</azimuth>
           <distance>0.00340932820804</distance>
-          <takeoffAngle>
-            <value>-1.0</value>
-          </takeoffAngle>
           <timeResidual>0.0025</timeResidual>
           <timeWeight>1.0009</timeWeight>
         </arrival>
         <arrival publicID="smi:local/c0778a50-dd6e-4080-9761-2aae8bbbcdab">
           <pickID>smi:local/9254790e-4e24-415f-b7a2-a60e504f3549</pickID>
           <phase>P</phase>
-          <azimuth>359.0</azimuth>
+          <azimuth>71.75</azimuth>
           <distance>0.00396331031728</distance>
-          <takeoffAngle>
-            <value>-1.0</value>
-          </takeoffAngle>
           <timeResidual>-0.0009</timeResidual>
           <timeWeight>1.0016</timeWeight>
         </arrival>
         <arrival publicID="smi:local/963aa4e6-2b6e-4978-8fcc-de9a9ec4891a">
           <pickID>smi:local/f744786a-bc96-4476-8c5c-6f2a9a5fef54</pickID>
           <phase>P</phase>
-          <azimuth>359.0</azimuth>
+          <azimuth>204.61</azimuth>
           <distance>0.00499483219927</distance>
-          <takeoffAngle>
-            <value>-1.0</value>
-          </takeoffAngle>
           <timeResidual>0.0065</timeResidual>
           <timeWeight>0.997</timeWeight>
         </arrival>
         <arrival publicID="smi:local/898783bb-82eb-45e6-84d1-86ca16c536bd">
           <pickID>smi:local/a040ecfb-0d12-4c91-9e37-463f075a2ec6</pickID>
           <phase>P</phase>
-          <azimuth>359.0</azimuth>
+          <azimuth>104.59</azimuth>
           <distance>0.00563874646911</distance>
-          <takeoffAngle>
-            <value>-1.0</value>
-          </takeoffAngle>
           <timeResidual>-0.0005</timeResidual>
           <timeWeight>1.0016</timeWeight>
         </arrival>
diff --git a/obspy/io/nlloc/tests/data/nlloc_custom.qml b/obspy/io/nlloc/tests/data/nlloc_custom.qml
--- a/obspy/io/nlloc/tests/data/nlloc_custom.qml
+++ b/obspy/io/nlloc/tests/data/nlloc_custom.qml
@@ -66,7 +66,7 @@ STATISTICS ExpectX 4473.68 Y 5323.29 Z 4.59501  CovXX 0.0282621 XY -0.0053866 XZ
         <arrival publicID="smi:local/677a937b-4db0-4fad-8822-9d75fdb3f9c5">
           <pickID>smi:local/d7ba3bb7-645f-4ee6-a8f4-65e0332c5025</pickID>
           <phase>P</phase>
-          <azimuth>200.7</azimuth>
+          <azimuth>195.71</azimuth>
           <distance>0.0169036489048</distance>
           <takeoffAngle>
             <value>152.6</value>
@@ -77,7 +77,7 @@ STATISTICS ExpectX 4473.68 Y 5323.29 Z 4.59501  CovXX 0.0282621 XY -0.0053866 XZ
         <arrival publicID="smi:local/06e00087-a9c8-4ee5-a5a8-4240bcac7db8">
           <pickID>smi:local/3a0bde89-d7e6-45ef-a08a-4b950720f7be</pickID>
           <phase>S</phase>
-          <azimuth>200.6</azimuth>
+          <azimuth>195.71</azimuth>
           <distance>0.0169036489048</distance>
           <takeoffAngle>
             <value>156.4</value>
@@ -88,7 +88,7 @@ STATISTICS ExpectX 4473.68 Y 5323.29 Z 4.59501  CovXX 0.0282621 XY -0.0053866 XZ
         <arrival publicID="smi:local/619921cf-129e-4ef8-a743-a397becd7146">
           <pickID>smi:local/95492f79-0db4-4bba-a198-6c08db43dd83</pickID>
           <phase>P</phase>
-          <azimuth>64.7</azimuth>
+          <azimuth>66.5</azimuth>
           <distance>0.0267620123489</distance>
           <takeoffAngle>
             <value>139.3</value>
@@ -99,7 +99,7 @@ STATISTICS ExpectX 4473.68 Y 5323.29 Z 4.59501  CovXX 0.0282621 XY -0.0053866 XZ
         <arrival publicID="smi:local/2c5ae0cc-981d-42c5-b2ef-57ae34ba5c02">
           <pickID>smi:local/75ca039c-bfca-41c3-8fd3-9d70341a23fe</pickID>
           <phase>S</phase>
-          <azimuth>63.9</azimuth>
+          <azimuth>66.5</azimuth>
           <distance>0.0267620123489</distance>
           <takeoffAngle>
             <value>144.4</value>
@@ -110,7 +110,7 @@ STATISTICS ExpectX 4473.68 Y 5323.29 Z 4.59501  CovXX 0.0282621 XY -0.0053866 XZ
         <arrival publicID="smi:local/44bbb2f9-76cc-4f73-9b8c-022ec9950b0b">
           <pickID>smi:local/550537fb-922c-4742-9bcc-504c1d330480</pickID>
           <phase>P</phase>
-          <azimuth>348.8</azimuth>
+          <azimuth>349.85</azimuth>
           <distance>0.0349881070783</distance>
           <takeoffAngle>
             <value>131.0</value>
@@ -121,7 +121,7 @@ STATISTICS ExpectX 4473.68 Y 5323.29 Z 4.59501  CovXX 0.0282621 XY -0.0053866 XZ
         <arrival publicID="smi:local/d7b02dea-3610-4209-9fe8-ab450a159c90">
           <pickID>smi:local/0783bf9f-6862-40a3-baeb-ed247b73ca6f</pickID>
           <phase>S</phase>
-          <azimuth>348.4</azimuth>
+          <azimuth>349.85</azimuth>
           <distance>0.0349881070783</distance>
           <takeoffAngle>
             <value>137.7</value>
@@ -132,7 +132,7 @@ STATISTICS ExpectX 4473.68 Y 5323.29 Z 4.59501  CovXX 0.0282621 XY -0.0053866 XZ
         <arrival publicID="smi:local/f7d77c25-1729-4879-b014-2f4608b72844">
           <pickID>smi:local/d6fa4a8a-6e4c-48a1-9ab9-e84b60038f4b</pickID>
           <phase>P</phase>
-          <azimuth>258.3</azimuth>
+          <azimuth>258.95</azimuth>
           <distance>0.0751742930387</distance>
           <takeoffAngle>
             <value>106.5</value>
@@ -143,7 +143,7 @@ STATISTICS ExpectX 4473.68 Y 5323.29 Z 4.59501  CovXX 0.0282621 XY -0.0053866 XZ
         <arrival publicID="smi:local/dbeb77a9-1f2a-4d81-adb0-a1ad82713665">
           <pickID>smi:local/b0b74d7c-bd9e-4183-abcb-5c784a53f8b0</pickID>
           <phase>S</phase>
-          <azimuth>258.1</azimuth>
+          <azimuth>258.95</azimuth>
           <distance>0.0751742930387</distance>
           <takeoffAngle>
             <value>113.1</value>
diff --git a/obspy/io/nlloc/tests/test_core.py b/obspy/io/nlloc/tests/test_core.py
--- a/obspy/io/nlloc/tests/test_core.py
+++ b/obspy/io/nlloc/tests/test_core.py
@@ -197,8 +197,8 @@ def test_read_nlloc_hyp_with_builtin_projection(self):
             self.assertEqual(arriv.phase, arriv_expected.phase)
             self.assertAlmostEqual(arriv.azimuth, arriv_expected.azimuth)
             self.assertAlmostEqual(arriv.distance, arriv_expected.distance)
-            self.assertAlmostEqual(arriv.takeoff_angle,
-                                   arriv_expected.takeoff_angle)
+            assert arriv.takeoff_angle is None
+            assert arriv_expected.takeoff_angle is None
             self.assertAlmostEqual(arriv.time_residual,
                                    arriv_expected.time_residual)
             self.assertAlmostEqual(arriv.time_weight,
@@ -347,4 +347,4 @@ def test_reading_nlloc_v7_hyp_file(self):
         """
         path = str(self.datapath / 'nlloc_v7.hyp')
         cat = read_nlloc_hyp(path)
-        assert cat[0].origins[0].arrivals[0].azimuth == 40
+        assert cat[0].origins[0].arrivals[0].azimuth == 107.42
diff --git a/obspy/io/seiscomp/tests/data/iris_events.sc3ml b/obspy/io/seiscomp/tests/data/iris_events.sc3ml
--- a/obspy/io/seiscomp/tests/data/iris_events.sc3ml
+++ b/obspy/io/seiscomp/tests/data/iris_events.sc3ml
@@ -3,7 +3,7 @@
   <EventParameters publicID="smi:www.iris.edu/ws/event/query">
     <origin publicID="smi:www.iris.edu/ws/event/query?originId=7680412">
       <time>
-        <value>2011-03-11T05:46:24.1200</value>
+        <value>2011-03-11T05:46:24.1200Z</value>
       </time>
       <latitude>
         <value>38.297</value>
@@ -20,7 +20,7 @@
     </origin>
     <origin publicID="smi:www.iris.edu/ws/event/query?originId=3881858">
       <time>
-        <value>2006-09-10T04:26:33.6100</value>
+        <value>2006-09-10T04:26:33.6100Z</value>
       </time>
       <latitude>
         <value>9.614</value>
diff --git a/obspy/io/seiscomp/tests/data/iris_events_res.xml b/obspy/io/seiscomp/tests/data/iris_events_res.xml
--- a/obspy/io/seiscomp/tests/data/iris_events_res.xml
+++ b/obspy/io/seiscomp/tests/data/iris_events_res.xml
@@ -4,7 +4,7 @@
     <event publicID="smi:www.iris.edu/ws/event/query?eventId=3279407">
       <origin publicID="smi:www.iris.edu/ws/event/query?originId=7680412">
         <time>
-          <value>2011-03-11T05:46:24.1200</value>
+          <value>2011-03-11T05:46:24.1200Z</value>
         </time>
         <latitude>
           <value>38.297</value>
@@ -30,7 +30,7 @@
     <event publicID="smi:www.iris.edu/ws/event/query?eventId=2318174">
       <origin publicID="smi:www.iris.edu/ws/event/query?originId=3881858">
         <time>
-          <value>2006-09-10T04:26:33.6100</value>
+          <value>2006-09-10T04:26:33.6100Z</value>
         </time>
         <latitude>
           <value>9.614</value>
diff --git a/obspy/io/seiscomp/tests/data/qml-example-1.2-RC3.sc3ml b/obspy/io/seiscomp/tests/data/qml-example-1.2-RC3.sc3ml
--- a/obspy/io/seiscomp/tests/data/qml-example-1.2-RC3.sc3ml
+++ b/obspy/io/seiscomp/tests/data/qml-example-1.2-RC3.sc3ml
@@ -7,7 +7,7 @@
         <value>1.0e-08</value>
       </amplitude>
       <timeWindow>
-        <reference>2007-10-10T14:40:39.055</reference>
+        <reference>2007-10-10T14:40:39.055Z</reference>
         <begin>0</begin>
         <end>0.51424</end>
       </timeWindow>
@@ -15,7 +15,7 @@
     </amplitude>
     <origin publicID="smi:nz.org.geonet/event/2806038g/origin/1">
       <time>
-        <value>2007-10-10T14:40:39.055</value>
+        <value>2007-10-10T14:40:39.055Z</value>
         <uncertainty>0.51424</uncertainty>
         <confidenceLevel>68.0</confidenceLevel>
       </time>
diff --git a/obspy/io/seiscomp/tests/data/qml-example-1.2-RC3.xml b/obspy/io/seiscomp/tests/data/qml-example-1.2-RC3.xml
--- a/obspy/io/seiscomp/tests/data/qml-example-1.2-RC3.xml
+++ b/obspy/io/seiscomp/tests/data/qml-example-1.2-RC3.xml
@@ -8,7 +8,7 @@
           <value>1.0e-08</value>
         </genericAmplitude>
         <timeWindow>
-          <reference>2007-10-10T14:40:39.055</reference>
+          <reference>2007-10-10T14:40:39.055Z</reference>
           <begin>0</begin>
           <end>0.51424</end>
         </timeWindow>
@@ -34,7 +34,7 @@
       </magnitude>
       <origin publicID="smi:nz.org.geonet/event/2806038g/origin/1">
         <time>
-          <value>2007-10-10T14:40:39.055</value>
+          <value>2007-10-10T14:40:39.055Z</value>
           <uncertainty>0.51424</uncertainty>
           <confidenceLevel>68.0</confidenceLevel>
         </time>