Repository: obspy/obspy

Issue:
AR picker fails for low amplitude data
Bug occurs in:
- Current obspy master, Linux, installed from source


This issue was raised on the gitter chat by @Dieselinium_twitter. The AR picker uses floats in the internal c-func, but gives incorrect pick-times when there are low amplitudes. I haven't fully investigated this, but I suspect it is a rounding error and could be fixed by using double-precision for some internal variables in the c-func.

@Dieselinium_twitter found this case after removing instrument response from his data, resulting in low amplitude ground-motion data.

The below minimal example is adapted from the [AR-picker tutorial](https://docs.obspy.org/tutorial/code_snippets/trigger_tutorial.html#ar-picker):

```python
import numpy as np
from obspy.core import read
from obspy.signal.trigger import ar_pick


tr1 = read('https://examples.obspy.org/loc_RJOB20050801145719850.z.gse2')[0]
tr2 = read('https://examples.obspy.org/loc_RJOB20050801145719850.n.gse2')[0]
tr3 = read('https://examples.obspy.org/loc_RJOB20050801145719850.e.gse2')[0]

for tr in [tr1, tr2, tr3]:
    tr.data = tr.data * 1e-6
    tr.data = tr.data.astype(np.float32)
    tr.plot()
df = tr1.stats.sampling_rate
p_pick, s_pick = ar_pick(tr1.data, tr2.data, tr3.data, df,
                         1.0, 20.0, 1.0, 0.1, 4.0, 1.0, 2, 8, 0.1, 0.2)
print(p_pick)
```

This plots each trace, just to make sure that the data can be represented at float32 (which they can).  This outputs a p-pick time of: `-0.10000000149011612`, when it would (without the gain) output `30.6350002289`.



Discussion:

Resolving patch:
diff --git a/obspy/signal/trigger.py b/obspy/signal/trigger.py
--- a/obspy/signal/trigger.py
+++ b/obspy/signal/trigger.py
@@ -34,6 +34,7 @@
 import warnings
 
 import numpy as np
+import scipy
 
 from obspy import UTCDateTime
 from obspy.signal.cross_correlation import templates_max_similarity
@@ -465,14 +466,31 @@ def ar_pick(a, b, c, samp_rate, f1, f2, lta_p, sta_p, lta_s, sta_s, m_p, m_s,
     :rtype: tuple
     :returns: A tuple with the P and the S arrival.
     """
-    # be nice and adapt type if necessary
-    a = np.ascontiguousarray(a, np.float32)
-    b = np.ascontiguousarray(b, np.float32)
-    c = np.ascontiguousarray(c, np.float32)
-
     if not (len(a) == len(b) == len(c)):
         raise ValueError("All three data arrays must have the same length.")
 
+    a = scipy.signal.detrend(a, type='linear')
+    b = scipy.signal.detrend(b, type='linear')
+    c = scipy.signal.detrend(c, type='linear')
+
+    # be nice and adapt type if necessary
+    a = np.require(a, dtype=np.float32, requirements=['C_CONTIGUOUS'])
+    b = np.require(b, dtype=np.float32, requirements=['C_CONTIGUOUS'])
+    c = np.require(c, dtype=np.float32, requirements=['C_CONTIGUOUS'])
+
+    # scale amplitudes to avoid precision issues in case of low amplitudes
+    # C code picks the horizontal component with larger amplitudes, so scale
+    # horizontal components with a common scaling factor
+    data_max = np.abs(a).max()
+    if data_max < 100:
+        a *= 1e6
+        a /= data_max
+    data_max = max(np.abs(b).max(), np.abs(c).max())
+    if data_max < 100:
+        for data in (b, c):
+            data *= 1e6
+            data /= data_max
+
     s_pick = C.c_int(s_pick)  # pick S phase also
     ptime = C.c_float()
     stime = C.c_float()


Test patch:
diff --git a/obspy/signal/tests/test_trigger.py b/obspy/signal/tests/test_trigger.py
--- a/obspy/signal/tests/test_trigger.py
+++ b/obspy/signal/tests/test_trigger.py
@@ -106,6 +106,30 @@ def test_ar_pick(self):
         self.assertEqual(err.exception.args[0],
                          "All three data arrays must have the same length.")
 
+    def test_ar_pick_low_amplitude(self):
+        """
+        Test ar_pick with low amplitude data
+        """
+        data = []
+        for channel in ['z', 'n', 'e']:
+            file = os.path.join(self.path,
+                                'loc_RJOB20050801145719850.' + channel)
+            data.append(np.loadtxt(file, dtype=np.float32))
+
+        # articially reduce signal amplitude
+        for d in data:
+            d /= 10.0 * d.max()
+
+        # some default arguments
+        samp_rate, f1, f2, lta_p, sta_p, lta_s, sta_s, m_p, m_s, l_p, l_s = \
+            200.0, 1.0, 20.0, 1.0, 0.1, 4.0, 1.0, 2, 8, 0.1, 0.2
+        ptime, stime = ar_pick(data[0], data[1], data[2], samp_rate, f1, f2,
+                               lta_p, sta_p, lta_s, sta_s, m_p, m_s, l_p, l_s)
+        self.assertAlmostEqual(ptime, 30.6350002289)
+        # seems to be strongly machine dependent, go for int for 64 bit
+        # self.assertAlmostEqual(stime, 31.2800006866)
+        self.assertEqual(int(stime + 0.5), 31)
+
     def test_trigger_onset(self):
         """
         Test trigger onset function