Repository: obspy/obspy

Issue:
Default instrument set to DISP if not known but VEL is simulated
Hi - it's not directly an issue but an inconvenient default setting. We are working more and more with rotational motion sensors which physical unit is rad/s. As we ask the manufacturers of the instrument to send stationXML or NRL lookups together with the equipment it would be best to include the rad/s and rad physical units also into obspy. This means, however, that the list of intputs and outputs in the obspy remove_response routine needs to be extended to rotation rate and rotation (if we want to be completely precise). 
A current "flaw" in obspy remove_response default settings is (at least for me) that if the instrument type is not known, it will assume its is a DISP sensor and simulates then as default a VEL sensor. I think  this is not a good strategy even when the first step IS mentioned in the docs. I prefer to have both defaults in the same physical space, which would result in less surprises - at least that's what I have done in my local copy. Cheers Jo


Discussion:
Hey @jwassermann I think changes in defaults for `remove_response` would best implemented after/during #2592 which will be after 1.3.0. I will tag this issue to make sure we revisit sometime after 1.3 is released.
From the evalresp manpage of the version we currently still use:

https://github.com/obspy/obspy/blob/221c7ef4a960e8001115a8cd7111dd5854239b21/obspy/signal/src/evalresp/evalresp.1#L209-L216

Didn't try it, but maybe we could use that `DEF` (default units) thingy, sounds like it doesn't add any integration/differentiation.

Looks like [evalresp (even in newest versions 5.0.0) has no handling for rotational units](https://github.com/iris-edu/evalresp/blob/315617bb06ea7fc6853f6abe435265e26c8ce191/libsrc/evalresp/input.c#L409-L501).
OK, for future reference, we just had a look and the issue is here:

https://github.com/obspy/obspy/blob/221c7ef4a960e8001115a8cd7111dd5854239b21/obspy/core/inventory/response.py#L1195-L1202

We could maybe see if when unknown units are encountered, we override `output` and set it to `DEF` (assumingly doing nothing). Overall it might still be problematic, since basically on unknown units we tell evalresp simply for every stage that input and output units are all the same.
It might be useful to deal with the case of polynomial responses as well.  These will usually be for pressure sensors ([example](http://service.iris.edu/irisws/resp/1/query?net=IU&sta=ANMO&loc=31&cha=LDO&starttime=2012-09-21T00:00:00&endtime=2599-12-31T23:59:59)).

Rough logic for dealing with this following.  I can make a pull request to include this after #2592 gets included.

```
def remove_polynomial(st, inv):
    # Simple function to remove the response of polynomial responses
    for tr in st.select(channel='LDO'):
        # Grab polynomial response
        resp = inv.get_response(tr.id, tr.stats.starttime).instrument_polynomial.coefficients
        # Go to floats to avoid type issues.
        tr.data = np.require(tr.data, dtype=np.float64)
        # Make a data vector of zeros
        data_temp = np.zeros_like(tr.data)
        # For each coefficient add the data to data_temp
        for idx, coef in enumerate(resp):
            data_temp += coef*(tr.data**idx) 

        tr.data = data_temp

    return
```
@aringler-usgs, aren't we already able to handle polynomial responses?? We just pass it into evalresp and both evalresp and us recognize pressure units. This issue is pretty much only about differentiation/integration potentially being done when they shouldn't at end of instrument response removal when the units of response stages are unknown to evalresp, as is the case for rotational motion sensors (units of RAD/S or RAD). Currently in said case we set these unkown units to DISP, so that we can still use evalresp and the output unit default of VEL can cause a potentially unwanted differentiation added at the end of response removal.
@megies Ack good catch.  Someone on a slack channel was having issues with the polynomial response.  However, in 1.2.2 the polynomial response works.  I should have had them check their code version.  Sorry for the confusion!

Resolving patch:
diff --git a/obspy/core/inventory/response.py b/obspy/core/inventory/response.py
--- a/obspy/core/inventory/response.py
+++ b/obspy/core/inventory/response.py
@@ -1195,12 +1195,15 @@ def get_unit_mapping(key):
                 "MBAR": ew.ENUM_UNITS["PRESSURE"]}
             if key not in units_mapping:
                 if key is not None:
-                    msg = ("The unit '%s' is not known to ObsPy. It will be "
-                           "assumed to be displacement for the calculations. "
-                           "This mostly does the right thing but please "
-                           "proceed with caution.") % key
+                    msg = (f"The unit '{key}' is not known to ObsPy. It will "
+                           f"be passed in to evalresp as 'undefined'. This "
+                           f"should result in evalresp using the response as "
+                           f"is, without adding any integration or "
+                           f"differentiation and the 'output' parameter "
+                           f"(here: '{output}') not having any effect. Please "
+                           f"double check output data.")
                     warnings.warn(msg)
-                value = ew.ENUM_UNITS["DIS"]
+                value = ew.ENUM_UNITS["UNDEF_UNITS"]
             else:
                 value = units_mapping[key]
 


Test patch:
diff --git a/obspy/core/tests/data/response_radian_per_second.xml b/obspy/core/tests/data/response_radian_per_second.xml
new file mode 100644
--- /dev/null
+++ b/obspy/core/tests/data/response_radian_per_second.xml
@@ -0,0 +1,101 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<FDSNStationXML xmlns="http://www.fdsn.org/xml/station/1" schemaVersion="1.0">
+  <Source></Source>
+  <Sender></Sender>
+  <Module></Module>
+  <ModuleURI></ModuleURI>
+  <Created>2000-01-01T00:00:00.000000Z</Created>
+  <Network code="XX">
+    <Description></Description>
+    <SelectedNumberStations>1</SelectedNumberStations>
+    <Station code="XX" startDate="2000-01-01T00:00:00.000000Z">
+      <Description></Description>
+      <Latitude unit="DEGREES">1.0</Latitude>
+      <Longitude unit="DEGREES">1.0</Longitude>
+      <Elevation unit="METERS">1.0</Elevation>
+      <Site>
+        <Name></Name>
+      </Site>
+      <CreationDate>2000-01-01T00:00:00.000000Z</CreationDate>
+      <TotalNumberChannels>1</TotalNumberChannels>
+      <SelectedNumberChannels>1</SelectedNumberChannels>
+      <Channel code="HJ1" locationCode="" startDate="2000-01-01T00:00:00.000000Z">
+        <Latitude unit="DEGREES">1.0</Latitude>
+        <Longitude unit="DEGREES">1.0</Longitude>
+        <Elevation unit="METERS">1.0</Elevation>
+        <Depth unit="METERS">0.0</Depth>
+        <Azimuth unit="DEGREES">90.0</Azimuth>
+        <Dip unit="DEGREES">0.0</Dip>
+        <SampleRate>200.0</SampleRate>
+        <Sensor>
+          <Type>FOG rotational sensor</Type>
+        </Sensor>
+        <Response>
+          <InstrumentSensitivity>
+            <Value>1000000000.0</Value>
+            <Frequency>0.02</Frequency>
+            <InputUnits>
+              <Name>RAD/S</Name>
+              <Description>Radians per Second</Description>
+            </InputUnits>
+            <OutputUnits>
+              <Name>COUNTS</Name>
+              <Description>Digital Counts</Description>
+            </OutputUnits>
+          </InstrumentSensitivity>
+          <Stage number="1">
+            <PolesZeros>
+              <InputUnits>
+                <Name>RAD/S</Name>
+                <Description>Radians per Second</Description>
+              </InputUnits>
+              <OutputUnits>
+                <Name>V</Name>
+                <Description>Volts</Description>
+              </OutputUnits>
+              <PzTransferFunctionType>LAPLACE (RADIANS/SECOND)</PzTransferFunctionType>
+              <NormalizationFactor>1.0</NormalizationFactor>
+              <NormalizationFrequency unit="HERTZ">1.0</NormalizationFrequency>
+              <Zero number="0">
+                <Real minusError="1.0" plusError="1.0">1.0</Real>
+                <Imaginary minusError="0.0" plusError="0.0">0.0</Imaginary>
+              </Zero>
+              <Pole number="0">
+                <Real minusError="1.0" plusError="1.0">1.0</Real>
+                <Imaginary minusError="0.0" plusError="0.0">0.0</Imaginary>
+              </Pole>
+            </PolesZeros>
+            <StageGain>
+              <Value>1.0</Value>
+              <Frequency>0.02</Frequency>
+            </StageGain>
+          </Stage>
+          <Stage number="2">
+            <Coefficients>
+              <InputUnits>
+                <Name>V</Name>
+                <Description>Volts</Description>
+              </InputUnits>
+              <OutputUnits>
+                <Name>COUNTS</Name>
+                <Description>Digital Counts</Description>
+              </OutputUnits>
+              <CfTransferFunctionType>DIGITAL</CfTransferFunctionType>
+            </Coefficients>
+            <Decimation>
+              <InputSampleRate unit="HERTZ">200.0</InputSampleRate>
+              <Factor>1</Factor>
+              <Offset>0</Offset>
+              <Delay>0.0</Delay>
+              <Correction>0.0</Correction>
+            </Decimation>
+            <StageGain>
+              <Value>1000000000.0</Value>
+              <Frequency>0.0</Frequency>
+            </StageGain>
+          </Stage>
+        </Response>
+      </Channel>
+    </Station>
+  </Network>
+</FDSNStationXML>
diff --git a/obspy/core/tests/test_response.py b/obspy/core/tests/test_response.py
--- a/obspy/core/tests/test_response.py
+++ b/obspy/core/tests/test_response.py
@@ -13,6 +13,7 @@
 import os
 import warnings
 from math import pi
+from pathlib import Path
 
 import numpy as np
 import pytest
@@ -35,8 +36,8 @@ class TestResponse:
     """
     Tests the for :class:`~obspy.core.inventory.response.Response` class.
     """
-    data_dir = os.path.join(os.path.dirname(os.path.abspath(
-               inspect.getfile(inspect.currentframe()))), "data")
+    data_dir = Path(os.path.join(os.path.dirname(os.path.abspath(
+                    inspect.getfile(inspect.currentframe()))), "data"))
 
     def test_evalresp_with_output_from_seed(self):
         """
@@ -519,3 +520,31 @@ def test_warning_response_list_extrapolation(self):
                "accordingly.")
         with CatchAndAssertWarnings(expected=[(UserWarning, msg)]):
             resp.get_evalresp_response(0.005, 2**3)
+
+    def test_unknown_units_no_integration(self):
+        """
+        Makes sure that when a unit in the list of response stages is not known
+        to evalresp, no tampering (integration/differentiation) is done and
+        response is just calculated as is specified.
+        It used to be that unkown units where reported to evalresp as
+        displacement ("DIS") which led to a differentiation being added
+        internally with the default "output='VEL'".
+        Example is a rotational sensor with input units RAD/S and a flat
+        response and since evalresp does not know RAD or RAD/S the only thing
+        that makes sense is have evalresp not do anything else than use the
+        response as is and ignore "output" option.
+        """
+        inv = read_inventory(self.data_dir / 'response_radian_per_second.xml',
+                             format="STATIONXML")
+        resp = inv[0][0][0].response
+        freqs = [0.01, 0.1, 1, 10, 100]
+        overall_sensitivity = 1.00000000e+09
+        msg = (r"The unit 'RAD/S' is not known to ObsPy. It will be passed in "
+               r"to evalresp as 'undefined'. This should result in evalresp "
+               r"using the response as is, without adding any integration or "
+               r"differentiation and the 'output' parameter \(here: 'VEL'\) "
+               r"not having any effect. Please double check output data.")
+        with pytest.warns(UserWarning, match=msg):
+            data = resp.get_evalresp_response_for_frequencies(freqs)
+        assert resp.instrument_sensitivity.value == overall_sensitivity
+        assert np.abs(data).tolist() == [overall_sensitivity] * len(freqs)