Repository: obspy/obspy

Issue:
signal->polarization: incorrect timestamp of window
The timestamp assigned to the time windows in polarization analysis is imho incorrect.

polarization.py presently:
```
540             # we plot against the centre of the sliding window
541             if method.lower() == "pm":
542                 azimuth, incidence, error_az, error_inc = \
543                     particle_motion_odr(data, var_noise)
544                 res.append(np.array([newstart.timestamp + float(nstep) / fs,
545                            azimuth, incidence, error_az, error_inc]))
546             if method.lower() == "flinn":
547                 azimuth, incidence, reclin, plan = flinn(data, var_noise)
548                 res.append(np.array([newstart.timestamp + float(nstep) / fs,
549                                     azimuth, incidence, reclin, plan]))
```

plotting against the centre of the sliding window (comment in line 540) makes full sense 
but then lines 544 and 548 should be:
```
                     res.append(np.array([newstart.timestamp + float(nsamp) / 2 / fs,
```

### Why?
- `nsamp` is no of samples of sliding window
- `nstep` is no of samples to shift window from one timestamp to the next

if `win_frac = 0.5` (fraction of sliding window), the original version is correct.
else the timestamp will not be the center of the current timewindow but the starttime of the next timewindow.

### Example 
with `win_len = 0.3` and `win_frac = 0.1`
- original: polarization signal is shifted to earlier times than signal peak in waveform (P-pol pick is not central)
![polarization original](https://github.com/cweidle/tmpimages/raw/main/syndata_BAZ_045_INC_90_pol_BUG.png)

- fixed: polarization signal is centered around P-pol pick
![polarization fixed](https://github.com/cweidle/tmpimages/raw/main/syndata_BAZ_045_INC_90_pol.png)




Discussion:
@cweidle or anybody else, happen to have the above data lying around still or have some other good example data + code to reproduce this? would help to save time trying to set up a case to check / work on this
sure, the example above is a tapered cosine wavelet on the L-component of a LQT-system (Q,T zero), rotated to ZNE for a given incidence angle (90° horizontal) and azimuth (45°).
[syndata_BAZ_045_INC_90.mseed.zip](https://github.com/obspy/obspy/files/9871399/syndata_BAZ_045_INC_90.mseed.zip)

In the example above I added 0.6 * max.amplitude of random noise. Without noise the SVD in polarization_analysis fails.
hope this helps.

Resolving patch:
diff --git a/obspy/signal/polarization.py b/obspy/signal/polarization.py
--- a/obspy/signal/polarization.py
+++ b/obspy/signal/polarization.py
@@ -514,6 +514,7 @@ def polarization_analysis(stream, win_len, win_frac, frqlow, frqhigh, stime,
         tap = cosine_taper(nsamp, p=0.22)
         offset = 0
         while (newstart + (nsamp + nstep) / fs) < etime:
+            timestamp = newstart.timestamp + (float(nsamp) / 2 / fs)
             try:
                 for i, tr in enumerate(stream):
                     dat = tr.data[spoint[i] + offset:
@@ -537,15 +538,15 @@ def polarization_analysis(stream, win_len, win_frac, frqlow, frqhigh, stime,
             if method.lower() == "pm":
                 azimuth, incidence, error_az, error_inc = \
                     particle_motion_odr(data, var_noise)
-                res.append(np.array([newstart.timestamp + float(nstep) / fs,
-                           azimuth, incidence, error_az, error_inc]))
+                res.append(np.array([
+                    timestamp, azimuth, incidence, error_az, error_inc]))
             if method.lower() == "flinn":
                 azimuth, incidence, reclin, plan = flinn(data, var_noise)
-                res.append(np.array([newstart.timestamp + float(nstep) / fs,
-                                    azimuth, incidence, reclin, plan]))
+                res.append(np.array([
+                    timestamp, azimuth, incidence, reclin, plan]))
 
             if verbose:
-                print(newstart, newstart + nsamp / fs, res[-1][1:])
+                print(newstart, newstart + float(nsamp) / fs, res[-1][1:])
             offset += nstep
 
             newstart += float(nstep) / fs


Test patch:
diff --git a/obspy/signal/tests/test_polarization.py b/obspy/signal/tests/test_polarization.py
--- a/obspy/signal/tests/test_polarization.py
+++ b/obspy/signal/tests/test_polarization.py
@@ -123,15 +123,18 @@ def test_polarization_pm(self):
         st = _create_test_data()
         t = st[0].stats.starttime
         e = st[0].stats.endtime
+        wlen = 10.0
+        wfrac = 0.1
 
         out = polarization.polarization_analysis(
-            st, win_len=10.0, win_frac=0.1, frqlow=1.0, frqhigh=5.0,
+            st, win_len=wlen, win_frac=wfrac, frqlow=1.0, frqhigh=5.0,
             verbose=False, stime=t, etime=e, method="pm",
             var_noise=0.0)
 
         # all values should be equal for the test data, so check first value
         # and make sure all values are almost equal
-        self.assertEqual(out["timestamp"][0], 1393632001.0)
+        self.assertEqual(out["timestamp"][0], 1393632005.0)
+        self.assertEqual(out["timestamp"][0], t + wlen / 2.0)
         self.assertAlmostEqual(out["azimuth"][0], 26.56505117707799)
         self.assertAlmostEqual(out["incidence"][0], 65.905157447889309)
         self.assertAlmostEqual(out["azimuth_error"][0], 0.000000)
@@ -152,15 +155,18 @@ def test_polarization_flinn(self):
         st = _create_test_data()
         t = st[0].stats.starttime
         e = st[0].stats.endtime
+        wlen = 10.0
+        wfrac = 0.1
 
         out = polarization.polarization_analysis(
-            st, win_len=10.0, win_frac=0.1, frqlow=1.0, frqhigh=5.0,
+            st, win_len=wlen, win_frac=wfrac, frqlow=1.0, frqhigh=5.0,
             verbose=False, stime=t, etime=e,
             method="flinn", var_noise=0.0)
 
         # all values should be equal for the test data, so check first value
         # and make sure all values are almost equal
-        self.assertEqual(out["timestamp"][0], 1393632001.0)
+        self.assertEqual(out["timestamp"][0], 1393632005.0)
+        self.assertEqual(out["timestamp"][0], t + wlen / 2.0)
         self.assertAlmostEqual(out["azimuth"][0], 26.56505117707799)
         self.assertAlmostEqual(out["incidence"][0], 65.905157447889309)
         self.assertAlmostEqual(out["rectilinearity"][0], 1.000000)