Trace.remove_response() outputting DISP or ACC

Hi

I’m trying to put together a script to simplify converting raw data to displacement, velocity, acceleration data. Previously we’ve done this mainly using sac (v101.6a). However using obspy I’m running into trouble reproducing the sac results for displacement and acceleration, whilst output of velocity is within acceptable tolerance. Presumably I’ve missed out on some important point here, so would appreciate any pointers (from what I (miss?)understand by digging through the obspy source obspy make use of evalresp as do sac and although sac stores data as 32-bit most the computations involved mainly uses 64-bit according to the sac manual, in any case the differences I see are to large to be explained by 32-bit vs 64-bit).

Attached here is a raw data (100 Hz sampling rate, 60s instrument) file, data_raw.sac (141.2 KB), as well as a RESP (4.4 KB) file used in the examples below (note that the numbers in the RESP file for the digitizer may look a bit strange, however the file is valid and used both in the sac and obspy instrument de-convolution so should not matter).

The sac results I’m comparing to were produced by use of commands:

displacement

SAC> r data_raw.sac
SAC> rtr
SAC> rmean
SAC> taper type hanning width 0.05
SAC> transfer from evalresp fname RESP freql 0.0117 0.0167 44.0 48.0
SAC> w data_disp_sac.sac

velocity

SAC> r data_raw.sac
SAC> rtr
SAC> rmean
SAC> taper type hanning width 0.05
SAC> transfer from evalresp fname RESP to vel freql 0.0117 0.0167 44.0 48.0
SAC> w data_vel_sac.sac

acceleration

SAC> r data_raw.sac
SAC> rtr
SAC> rmean
SAC> taper type hanning width 0.05
SAC> transfer from evalresp fname RESP to acc freql 0.0117 0.0167 44.0 48.0
SAC> w data_acc_sac.sac

To further aid the comparison of the sac and obspy results I’ve put together a small function to plot the amplitude spectrum (btw. is such a function available in obspy?) in a simple module
ps.py (1.2 KB):

#! /usr/bin/env python3

import numpy as np
import matplotlib.pyplot as plt
def plot_spectrum(st):
   """ plots amplitude spectra """
   axs = []
   ntr = len(st)      
   fig = plt.figure(figsize=(14, 10))
   d = []
   for i in range(ntr):
      tr = st[i]
      if i in [2,4]:
         d.append(d[0]-d[i-1])
      else:
         data = tr.data.astype(np.float64)
         fft = np.fft.rfft(data)
         d.append(np.abs(fft))
      fs = [j*tr.stats.sampling_rate/tr.stats.npts for j in range(fft.size)]
      axs.append(fig.add_subplot(ntr,1,i+1))
      axs[-1].semilogy(fs,d[i])
      axs[-1].set_xlim([0,fs[-1]])
      axs[-1].set_ylim([0.01,10**7])
      axs[-1].text(0.02,0.95,tr.id,transform=axs[-1].transAxes,
                  fontdict=dict(fontsize="small", ha='left', va='top'),
                  bbox=dict(boxstyle="round", fc="w", alpha=0.8))
      axs[-1].tick_params(axis="x",which='both',direction="in",top=True,right=True)
      axs[-1].tick_params(axis="y",which='both',direction="in",top=True,right=True)
      if i == (ntr)//2:
         axs[-1].set_ylabel("Amplitude")
      if i < ntr-1:
         axs[-1].xaxis.set_ticklabels([])
   axs[-1].set_xlabel("Frequency (Hz)")
      
   fig.tight_layout()
   fig.subplots_adjust(hspace=0)

obspy velocity
Conversion to velocity by use of Trace.remove_response(...) works as a charm in terms of comparison to the sac results, difference between sac and obspy results are < 0.1% except at the end points where small differences in tapers used probably is the explanation. For completeness though I add here the script, vel.py (1.3 KB), used.

obspy displacements
Conversion to displacement using Trace.remove_response(output="DISP", ...) however yields differences of the order 100% in the time domain and whilst the amplitude spectrum has similar shape there is a significant difference in the amplitude value. For the fun of it I then tried to simply remove the response (counts → velocity) followed by tr.integrate() and strangely enough, suddenly the resulting displacement is within acceptable tolerance (mostly < 1%). The script to reproduce the results, disp.py (1.8 KB), is:

#! /usr/bin/env python3
import obspy
from ps import plot_spectrum

### sac (v101.6a) estimate produced by use of commands:  
#  SAC> r data_raw.sac
#  SAC> rtr
#  SAC> rmean
#  SAC> taper type hanning width 0.05
#  SAC> transfer from evalresp fname RESP freql 0.0117 0.0167 44.0 48.0
#  SAC> w data_disp_sac.sac

### Get raw data and respospnse, attach response, remove linear trend, demean and taper
tr = obspy.read("data_raw.sac")[0]
inv = obspy.read_inventory("RESP")
tr.attach_response(inv)
tr.detrend(type="linear")
tr.detrend(type="demean")
tr.taper(type='hann',max_percentage=0.05)

### get two copies of the trace (set meta data to be able to identify in later plot)
tr1 = tr.copy()
tr1.stats.network = "tr1"
tr2 = tr.copy()
tr2.stats.network = "tr2"

### Get displacemnet data as converted by use of SAC (set meta data to be able to identify in later plot)
st = obspy.read("data_disp_sac.sac")
st[0].stats.network = "sac"
st[0].detrend(type="demean")

### Remove response and convert to displacement (nm) in one go

tr.taper(type='hann',max_percentage=0.05)
tr1.remove_response(output="DISP", pre_filt=[0.0117,0.0167,44.0,48.0], zero_mean=True, taper=False)
tr1.data *= 1.e9
tr1.detrend(type="demean")

### Remove response and integrate to displacements (nm) in two steps

tr.taper(type='hann',max_percentage=0.05)
tr2.remove_response(output="VEL", pre_filt=[0.0117,0.0167,44.0,48.0], zero_mean=True, taper=False)
tr2.data *= 1.e9
tr2.integrate()
tr2.detrend(type="demean")

### Compute difference to sac estimate
diff1 = tr1.copy()
diff1.data -= st[0].data
diff1.stats.network = "diff1"
diff2 = tr2.copy()
diff2.data -= st[0].data
diff2.stats.network = "diff2"

### Add obspy estimates and differences to stream object and plot
st += tr1
st += diff1
st += tr2
st += diff2
plot_spectrum(st)
st.plot(automerge=False,equal_scale=False)

obspy acceleration
Attempting to get to acceleration again there is a significant difference when using Trace.remove_response(output="ACC",...), further inspecting the amplitude spectrum there is a significant increase in the amplitude difference above ~23 Hz. Attempting a similar scheme as used above for the displacements (i.e. only removing response by use of Trace.remove_response(...) followed by Trace.integrate()) unfortunately do not resolve the issue. The script, acc.py (1.9 KB), to reproduce the results is:

#! /usr/bin/env python
import obspy
from ps import plot_spectrum

### sac (v101.6a) estimate produced by use of commands:  
#  SAC> r data_raw.sac
#  SAC> rtr
#  SAC> rmean
#  SAC> taper type hanning width 0.05
#  SAC> transfer from evalresp fname RESP to acc freql 0.0117 0.0167 44.0 48.0
#  SAC> w data_acc_sac.sac

### Get raw data and respospnse, attach response, remove linear trend, demean and taper
tr = obspy.read("data_raw.sac")[0]
inv = obspy.read_inventory("RESP")
tr.attach_response(inv)
tr.detrend(type="linear")
tr.detrend(type="demean")
tr.taper(type='hann',max_percentage=0.05)

### get two copies of the trace (set meta data to be able to identify in later plot)
tr1 = tr.copy()
tr1.stats.network = "tr1"
tr2 = tr.copy()
tr2.stats.network = "tr2"

### Get displacemnet data as converted by use of SAC (set meta data to be able to identify in later plot)
st = obspy.read("data_acc_sac.sac")
st[0].stats.network = "sac"
st[0].detrend(type="demean")
#st[0].filter("bandpass",freqmin=0.0167,freqmax=44)

### Remove response and convert to displacement (nm) in one go
tr1.remove_response(output="ACC", pre_filt=[0.0117,0.0167,44.0,48.0], zero_mean=False, taper=False)
tr1.data *= 1.e9
tr1.detrend(type="demean")
#tr1.filter("bandpass",freqmin=0.0167,freqmax=44)

### Remove response and integrate to displacements (nm) in two steps
tr2.remove_response(output="VEL", pre_filt=[0.0117,0.0167,44.0,48.0], zero_mean=False, taper=False)
tr2.data *= 1.e9
tr2.differentiate()
tr2.detrend(type="demean")
#tr2.filter("bandpass",freqmin=0.0167,freqmax=44)

### Compute difference to sac estimate
diff1 = tr1.copy()
diff1.data -= st[0].data
diff1.stats.network = "diff1"
diff2 = tr2.copy()
diff2.data -= st[0].data
diff2.stats.network = "diff2"

### Add obspy estimates and differences to stream object and plot
st += tr1
st += diff1
st += tr2
st += diff2
plot_spectrum(st)
st.plot(automerge=False)

So in summary I can get reasonable results for conversion to velocity and displacement but fail for acceleration (and would preffer to avoid the workaround when converting to displacements) so would appreciate any insight.

Hi, I tried to reproduce your issue, but I do not have SAC at hand. Can you provide the processed SAC files and maybe also post an image illustrating the issue?

Hi

Thanks for the response, apologies for not posting all required input files directly nor any of the figures, here they come:

sac files
data_disp_sac.sac (141.2 KB)
data_vel_sac.sac (141.2 KB)
data_acc_sac.sac (141.2 KB)

figures
In captions sac... is conversion by sac (compared to), tr1... is conversion by use of Trace.remove_response(output="...", ...), tr2…is conversion count -> vel by use ofTrace.remove_response()followed byTrace.integrate()orTrace.differentiate(), diffX…istrX - sacin time-domain andFFT(sac) - FFT(trX)` in frequency domain .

Displacement

displacement amplitude spectrum

Velocity

Velocity amplitude spectrum

Acceleration

Acceleration amplitude spectrum

Thanks for posting files and images!

You can use the plot keyword in remove_response to see what is going on.

For displacement:

The water level is making problems together with the integration in frequency domain resulting in distortion for low frequencies. Water level is probably only useful for restitution to the “natural” observable of the instrument.

Setting water_level=None fixes your problem.

The same is applicable to the acceleration. Here the water level distorts high frequencies.

We still see a difference, when the trace is restituted to velocity and afterwards differentiated to acceleration. I think both methods are different for discrete time series, because differentiation in time domain is implemented as difference of samples (obspy uses numpy gradient method which uses second order differences) which is not the same:

F(s[t+𝛥t] - s[t]) = (e^(i 𝛥t ω)-1) *F(s[t]) ≈ i 𝛥t ω F(s[t])
(time shift property)

The approximation is only valid for 𝛥t ω << 1, which does not hold near the Nyquist frequency. Therefore, a difference at high frequencies can be expected, I guess.

Ah great, thanks.

Didn’t have a good understanding of what were meant by water_level but this makes sense

Guess there will always small differences when using different software’s so with water_level=None I’d say that the remaining difference’s are acceptable.

Just quickly (as the question probably drowned above). Is there a method (or function) available in obspy (sorry I failed to spot it) to plot the amplitude (and/or phase) spectrum of a trace?

regards

No, unfortunately there is none at the moment. Only spectrogram and PPSD plots.

Ok, good to know

(adding some characters as posts needs to be at least 20 characters and I’m not fond of smileys, sorry…)