Repository: obspy/obspy

Issue:
PPSD with special_handling="ringlaser"
Hi,
I am using obspy1.1.0 with python3.6 installed with anaconda

 Scaling with the instrument sensitivity seams to fail, when using the kwarg `special_handling="ringlaser"`, in the call of PPSD(), 

running the following code 

``` #! /usr/bin/env python
 
 from obspy import read
 from obspy.signal import PPSD
 from obspy import read_inventory
 from obspy.imaging.cm import pqlx
 
 inv = read_inventory("https://examples.obspy.org/BW_KW1.xml")
 
 st = read("https://examples.obspy.org/BW.KW1..EHZ.D.2011.037")
 tr = st.select(id="BW.KW1..EHZ")[0]
 
 ppsd = PPSD(tr.stats, metadata=inv, special_handling='ringlaser') # fails
 #ppsd = PPSD(tr.stats, metadata=inv)  # works fine!
 
 ppsd.add(st)
 
 ppsd.plot(cmap=pqlx, show_noise_models=False, xaxis_frequency=True)
```
gives me the following error:

```(py36) fbernauer@hochfelln:~/Experiments/Huddle_BS123/PPSD$ ./make_ppsd_example.py 
Traceback (most recent call last):
  File "./make_ppsd_example.py", line 16, in <module>
    ppsd.add(st)
  File "/home/fbernauer/anaconda2/envs/py36/lib/python3.6/site-packages/obspy/signal/spectral_estimation.py", line 733, in add
    success = self.__process(slice)
  File "/home/fbernauer/anaconda2/envs/py36/lib/python3.6/site-packages/obspy/signal/spectral_estimation.py", line 799, in __process
    spec /= self.metadata['sensitivity'] ** 2
  File "/home/fbernauer/anaconda2/envs/py36/lib/python3.6/site-packages/obspy/core/inventory/inventory.py", line 168, in __getitem__
    return self.networks[index]
TypeError: list indices must be integers or slices, not str
```
I know that the example does not make sense, but it is the easiest way to show what I mean.
It seams to be linked to the issue posted by Keowne 

> Documentation for PPSD #2116

My work around is to define a dictionary as inventory:
```
inv = {'sensitivity': 1.e9}
```
Passing this as `metadata=inv` works. But it would be great to have it working with an inventory read in from a station.xml file e.g.

Thanks in advance
Felix



Discussion:
These `ringlaser` and `hydrophone` switches are kind of an ugly mechanic from back when `PPSD` wasn't even able to read StationXML, I think.

In any case, the `ringlaser` switch expects a plain old PAZ dict, it seems and can't cope with the `metadata` having been extended to StationXML etc. I'm assuming you have a StationXML or dataless SEED for the ringlaser nowadays that properly has a plain, flat amplitude response across all frequencies and that you're inputting to PPSD? I guess for that case, ideally there should be a better and more general, canonical solution. Currently the subroutines like `PPSD._get_response_from_inventory` mostly use evalresp which in turn only understands conversion for `ACC`, `VEL` and `DIS` (I think?) while here we have some other units entirely..

So TLDR: I think the only fix is to put in a simple dictionary like you did, which is easiest anyway. Maybe the docstring of `special_handling` should mention this, I guess.
Actually, the docstring for `special_handling` seems pretty OK..

```
 287         :type special_handling: str, optional
 288         :param special_handling: Switches on customized handling for
 289             data other than seismometer recordings. Can be one of: 'ringlaser'
 290             (no instrument correction, just division by
 291             `metadata["sensitivity"]` of provided metadata dictionary),
 292             'hydrophone' (no differentiation after instrument correction).
```

.. but I guess a nicer error message could be useful

Resolving patch:
diff --git a/obspy/signal/spectral_estimation.py b/obspy/signal/spectral_estimation.py
--- a/obspy/signal/spectral_estimation.py
+++ b/obspy/signal/spectral_estimation.py
@@ -320,7 +320,15 @@ def __init__(self, stats, metadata, skip_on_gaps=False,
         self.ppsd_length = ppsd_length
         self.overlap = overlap
         self.special_handling = special_handling and special_handling.lower()
-        if self.special_handling not in (None, "ringlaser", "hydrophone"):
+        if self.special_handling == 'ringlaser':
+            if not isinstance(self.metadata, dict):
+                msg = ("When using `special_handling='ringlaser'`, `metadata` "
+                       "must be a plain dictionary with key 'sensitivity' "
+                       "stating the overall sensitivity`.")
+                raise TypeError(msg)
+        elif self.special_handling == 'hydrophone':
+            pass
+        elif self.special_handling is not None:
             msg = "Unsupported value for 'special_handling' parameter: %s"
             msg = msg % self.special_handling
             raise ValueError(msg)


Test patch:
diff --git a/obspy/signal/tests/test_spectral_estimation.py b/obspy/signal/tests/test_spectral_estimation.py
--- a/obspy/signal/tests/test_spectral_estimation.py
+++ b/obspy/signal/tests/test_spectral_estimation.py
@@ -16,7 +16,7 @@
 
 import numpy as np
 
-from obspy import Stream, Trace, UTCDateTime, read, read_inventory
+from obspy import Stream, Trace, UTCDateTime, read, read_inventory, Inventory
 from obspy.core import Stats
 from obspy.core.inventory import Response
 from obspy.core.util.base import NamedTemporaryFile
@@ -817,6 +817,15 @@ def test_exception_reading_newer_npz(self):
                     method(filename)
                 self.assertEqual(str(e.exception), msg)
 
+    def test_nice_ringlaser_metadata_error_msg(self):
+        with self.assertRaises(TypeError) as e:
+            PPSD(stats=Stats(), metadata=Inventory(networks=[], source=""),
+                 special_handling='ringlaser')
+        expected = ("When using `special_handling='ringlaser'`, `metadata` "
+                    "must be a plain dictionary with key 'sensitivity' "
+                    "stating the overall sensitivity`.")
+        self.assertEqual(str(e.exception), expected)
+
 
 def suite():
     return unittest.makeSuite(PsdTestCase, 'test')