def bandwidth(data, fs, smoothie, fk): """ Bandwidth of a signal. Computes the bandwidth of the given data which can be windowed or not. The bandwidth corresponds to the level where the power of the spectrum is half its maximum value. It is determined as the level of 1/sqrt(2) times the maximum Fourier amplitude. If data are windowed the bandwidth of each window is returned. :type data: :class:`~numpy.ndarray` :param data: Data to make envelope of. :param fs: Sampling frequency in Hz. :param smoothie: Factor for smoothing the result. :param fk: Coefficients for calculating time derivatives (calculated via central difference). :return: **bandwidth[, dbwithd]** - Bandwidth, Time derivative of predominant period (windowed only). """ new_dtype = np.float32 if data.dtype.itemsize == 4 else np.float64 data = np.require(data, dtype=new_dtype) nfft = util.next_pow_2(data.shape[1]) freqaxis = np.linspace(0, fs, nfft + 1) bwith = np.zeros(data.shape[0]) f = fftpack.fft(data, nfft) f_sm = util.smooth(abs(f[:, 0:nfft // 2]), 10) if np.size(data.shape) > 1: i = 0 for row in f_sm: minfc = abs(row - max(abs(row * (1 / np.sqrt(2))))) [mdist_ind, _mindist] = min(enumerate(minfc), key=itemgetter(1)) bwith[i] = freqaxis[mdist_ind] i = i + 1 # bwith_add = \ # np.append(np.append([bandwidth[0]] * (np.size(fk) // 2), bandwidth), # [bandwidth[np.size(bandwidth) - 1]] * (np.size(fk) // 2)) # faster alternative bwith_add = np.hstack( ([bwith[0]] * (np.size(fk) // 2), bwith, [bwith[np.size(bwith) - 1]] * (np.size(fk) // 2))) dbwith = signal.lfilter(fk, 1, bwith_add) # dbwith = dbwith[np.size(fk) // 2:(np.size(dbwith) - # np.size(fk) // 2)] # correct start and end values of time derivative dbwith = dbwith[np.size(fk) - 1:] bwith = util.smooth(bwith, smoothie) dbwith = util.smooth(dbwith, smoothie) return bwith, dbwith else: minfc = abs(data - max(abs(data * (1 / np.sqrt(2))))) [mdist_ind, _mindist] = min(enumerate(minfc), key=itemgetter(1)) bwith = freqaxis[mdist_ind] return bwith
def calc_fft(str1, type, shift): if type is not None: data = str1[0].data N1 = len(data) data = data.astype(type) # Always use 2**n-sized FFT, perform xcorr size = max(2 * shift + 1, (N1) + shift) nfft = next_pow_2(size) print ("station %s and network %s - size in calc_fft %s " % (str1[0].stats['station'], str1[0].stats['network'], size)) #Calculate fft of data1 and data2 IN1 = fft(data, nfft) return IN1 else: return str1[0].data
def whiten(tr,freq1,freq2,taper_samples): # zeropadding should make things faster n_pad = next_pow_2(tr.stats.npts) data = my_centered(tr.data,n_pad) freqaxis=np.fft.rfftfreq(tr.stats.npts,tr.stats.delta) ind_fw = np.where( ( freqaxis > freq1 ) & ( freqaxis < freq2 ) )[0] if len(ind_fw) == 0: return(np.zeros(tr.stats.npts)) ind_fw1 = ind_fw[0] ind_fw2 = ind_fw[-1] # Build a cosine taper for the frequency domain #df = 1/(tr.stats.npts*tr.stats.delta) # Taper white_tape = whiten_taper(ind_fw1,ind_fw2,len(freqaxis),taper_samples) # Transform data to frequency domain tr.taper(max_percentage=0.05, type='cosine') spec = np.fft.rfft(tr.data) # Don't divide by 0 #tol = np.max(np.abs(spec)) / 1e5 #spec /= np.abs(spec+tol) # whiten. This elegant solution is from MSNoise: spec = white_tape * np.exp(1j * np.angle(spec)) # Go back to time domain # Difficulty here: The time fdomain signal might no longer be real. # Hence, irfft cannot be used. spec_neg = np.conjugate(spec)[::-1] spec = np.concatenate((spec,spec_neg[:-1])) tr.data = np.real(np.fft.ifft(spec))
def log_cepstrum(data, fs, nc, p, n, w): # @UnusedVariable: n is never used!!! """ Cepstrum of a signal. Computes the cepstral coefficient on a logarithmic scale of the given data which can be windowed or not. If data are windowed the analytic signal and the envelope of each window is returned. :type data: :class:`~numpy.ndarray` :param data: Data to make envelope of. :param fs: Sampling frequency in Hz. :param nc: number of cepstral coefficients. :param p: Number of filters in filterbank. :param n: Number of data windows. :return: Cepstral coefficients. """ new_dtype = np.float32 if data.dtype.itemsize == 4 else np.float64 data = np.require(data, dtype=new_dtype) dataT = np.transpose(data) nfft = util.next_pow_2(dataT.shape[0]) fc = fftpack.fft(dataT, nfft, 0) f = fc[1:len(fc) // 2 + 1, :] m, a, b = log_spaced_filterbank_matrix(p, nfft, fs, w) pw = np.real(np.multiply(f[a:b, :], np.conj(f[a:b, :]))) pth = np.max(pw) * 1E-20 ath = np.sqrt(pth) # h1 = np.transpose(np.array([[ath] * int(b + 1 - a)])) # h2 = m * abs(f[a - 1:b, :]) y = np.log(np.maximum(m * abs(f[a - 1:b, :]), ath)) z = util.rdct(y) z = z[1:, :] # nc = nc + 1 nf = np.size(z, 1) if (p > nc): z = z[:nc, :] elif (p < nc): z = np.vstack([z, np.zeros(nf, nc - p)]) return z
def spectralwhitening(stream): """ Apply spectral whitening to data. Data is divided by its smoothed (Default: None) amplitude spectrum. """ stream2 = copy.deepcopy(stream) for trace in arange(len(stream2)): data = stream2[trace].data n = len(data) nfft = next_pow_2(n) spec = fft(data, nfft) spec_ampl = sqrt(abs(multiply(spec, conjugate(spec)))) spec /= spec_ampl #Do we need to do some smoothing here? ret = real(ifft(spec, nfft)[:n]) stream2[trace].data = ret return stream2
def central_frequency_unwindowed(data, fs): """ Central frequency of a signal. Computes the central frequency of the given data (a single waveform). The central frequency is a measure of the frequency where the power is concentrated. It corresponds to the second moment of the power spectral density function. The central frequency is returned in Hz. :type data: :class:`~numpy.ndarray` :param data: Data to estimate central frequency from. :param fs: Sampling frequency in Hz. :return: **cfreq** - Central frequency in Hz """ nfft = util.next_pow_2(len(data)) freq = np.linspace(0, fs, nfft + 1) freqaxis = freq[0:nfft // 2] Px_wm = welch(data, np.hamming(len(data)), nfft) Px = Px_wm[0:len(Px_wm) // 2] cfreq = np.sqrt(np.sum(freqaxis ** 2 * Px) / (sum(Px))) return cfreq
def compute_signal_spectrum(tr, bandwidth): """ Compute raw and smoothed signal spectrum. Args: tr (StationTrace): Trace of data. bandwidth (float): Konno-Omachi smoothing bandwidth parameter. Returns: StationTrace with signal spectrum dictionaries added as trace parameters. """ # Transform to frequency domain and smooth spectra using # konno-ohmachi smoothing nfft = next_pow_2(tr.stats.npts) dt = tr.stats.delta sig_spec = abs(np.fft.rfft(tr.data, n=nfft)) * dt sig_spec_freqs = np.fft.rfftfreq(nfft, dt) sig_dict = { 'spec': sig_spec.tolist(), 'freq': sig_spec_freqs.tolist() } tr.setParameter('signal_spectrum', sig_dict) sig_spec_smooth, freqs_signal = fft_smooth( tr, nfft, bandwidth) smooth_dict = { 'spec': sig_spec_smooth.tolist(), 'freq': freqs_signal.tolist() } tr.setParameter('smooth_signal_spectrum', smooth_dict) return tr
def xcorrf(trace1, trace2, shift=None): """ Cross-correlation of numpy arrays data1 and data2 in frequency domain. """ data1 = trace1.data data2 = trace2.data complex_result = (data1.dtype == complex or data2.dtype == complex) N1 = len(data1) N2 = len(data2) data1 = data1.astype('float64') data2 = data2.astype('float64') # Always use 2**n-sized FFT, perform xcorr size = max(2 * shift + 1, (N1 + N2) // 2 + shift) nfft = next_pow_2(size) print size #Calculate fft of data1 and data2 IN1 = fft(data1, nfft) IN2 = fft(data2, nfft) IN1 *= conjugate(IN2) ret = ifft(IN1) del IN1, IN2 if not complex_result: ret = ret.real # shift data for time lag 0 to index 'shift' ret = roll(ret, -(N1 - N2) // 2 + shift)[:2 * shift + 1] return copy(ret)
def backproject(st, v, tshifts, freqmin, freqmax, winlen, overlap, gridx, gridy, gridz=None, starttime=None, endtime=None, relshift=0., coords=None, ds=False): ## COULD EASILY OUTPUT ALL FREQUENCIES, BUT ONLY VALID FOR THE ONE THE TIMESHIFTS OR SPEED ARE FOR """ st should have coordinates embedded, if not, coords need to be supplied in teh form of ? v, constant velocity to use to estimate travel times in half space set to None if tshifts are specified tshifts, time shifts, in seconds, from each station to each grid point in same order as traces in st, set to None if using constant velocity freqmin - lowest frequency to consider freqmax - higher frequency to consider gridxyz - grid of points to search over for backprojection winlen - window length in seconds overlap - amount to overlap windows from 0 (no overlap) to 1 (completely overlapped) coords - station coordinates, if not embedded in stream, only need for mapping and for computing travel times, not required if tshifts are provided. Should be in form of dictionary {sta: (x,y,elev)}, be sure units are consistent between v and coords ds - If true, will downsample to 1/(fhigh*2) starttime - start time in UTC of analysis, if None, will use start time of st endtime - same as above but end time relshift - relative shift to apply to all stations to account for travel time to first station """ # Initial data processing samprates = [trace.stats.sampling_rate for trace in st] if np.mean(samprates) != samprates[0] and ds is False: print('sample rates are not all equal, resampling to minimum sample rate') st = st.resample(np.min(samprates)) if ds is True: st = st.resample(1./(freqmax*2.)) dt = st[0].stats.sampling_rate if starttime is None: starttime = np.min([trace.stats.starttime for trace in st]) if endtime is None: endtime = np.max([trace.stats.endtime for trace in st]) st.trim(starttime, endtime, pad=True) # turns into masked array if needs to pad, might need to check if this breaks things nsta = len(st) if gridz is None: gridz = np.zeros(np.shape(gridx)) # if no elevations provided, assume all at zero elevation # Pull out coords for later usage sx = [] sy = [] selev = [] names = [] chan = [] if coords is None: for trace in st: sx.append(trace.stats.coordinates['x']) sy.append(trace.stats.coordinates['y']) selev.append(trace.stats.coordinates['elevation']) names.append(trace.stats.station) chan.append(trace.stats.channel) else: for trace in st: sx.append(coords[trace.stats.station][0]) sy.append(coords[trace.stats.station][1]) selev.append(coords[trace.stats.station][2]) names.append(trace.stats.station) chan.append(trace.stats.channel) nwins = int((endtime-starttime)/(winlen*(1.-overlap))) - 3 # look into why 3 incr = (1.-overlap)*winlen sttm = np.arange(0., incr*nwins, incr) winlensamp = int(np.round(winlen*dt)) nfft = 2*next_pow_2(winlen*dt) freqs = np.fft.fftfreq(nfft, 1/dt) freqsubl = len(freqs[(freqs >= freqmin) & (freqs <= freqmax)]) power = np.zeros((len(gridx), nwins, freqsubl)) powernon = power.copy() meanpow = np.zeros((len(gridx), nwins)) # Compute stack power at each hammer location for each point in time (need to figure out how to do ARF for this) for n in np.arange(nwins): reftime = starttime + sttm[n] for i, (gx, gy, gz) in enumerate(zip(gridx, gridy, gridz)): # Figure out what shifts are if v is not None and tshifts is None: shifts = np.sqrt((sx-gx)**2 + (sy-gy)**2 + (selev-gz)**2)/v elif tshifts is not None and v is None: shifts = tshifts + relshift else: print('neither tshifts or v defined properly') # Cut out window at relative start time sttemp = st.copy().trim(starttime=reftime) # Apply shifts, in time domain for grid point i extract = np.zeros((len(sttemp), freqsubl)) + 1j*np.zeros((len(sttemp), freqsubl)) extractnon = extract.copy() k = 0 for tr, shift in zip(sttemp, shifts): tr.detrend('demean') tr.taper(max_percentage=0.05, type='cosine') x = tr.data[:winlensamp] s = int(np.round(shift * tr.stats.sampling_rate)) N = nfft r = np.floor(N/2)+1 f = (np.arange(1, N+1)-r)/(N/2) p = np.exp(-1j*s*np.pi*f) # seems like it needs negative sign to shift in the right direction... y = fft(x, nfft)*ifftshift(p) ynon = fft(x, nfft) # whitening? #y = y/np.abs(y) #indx = np.where((freqs >= freqmin) & (freqs <= freqmax)) extract[k, :] = y[(freqs >= freqmin) & (freqs <= freqmax)] extractnon[k, :] = ynon[(freqs >= freqmin) & (freqs <= freqmax)] #import pdb;pdb.set_trace() k += 1 # extract mean beam power over this frequency range and add to cumulative total for this point power[i, n, :] = ((1./nsta) * np.abs(np.sum(extract, 0)))**2 powernon[i, n, :] = ((1./nsta) * np.abs(np.sum(extractnon, 0)))**2 meanpow[i, n] = np.mean(power[i, n, :])/np.mean(powernon[i, n, :]) print('%i of %i' % (n, nwins)) fvec = freqs[(freqs >= freqmin) & (freqs <= freqmax)] tvec = sttm + incr/2. return power, meanpow, tvec, fvec
def compute_snr_trace(tr, bandwidth, mag=None, check=None): if tr.hasParameter('signal_split'): # Split the noise and signal into two separate traces split_prov = tr.getParameter('signal_split') if isinstance(split_prov, list): split_prov = split_prov[0] split_time = split_prov['split_time'] noise = tr.copy().trim(endtime=split_time) signal = tr.copy().trim(starttime=split_time) noise.detrend('demean') signal.detrend('demean') # Taper both windows noise.taper(max_percentage=TAPER_WIDTH, type=TAPER_TYPE, side=TAPER_SIDE) signal.taper(max_percentage=TAPER_WIDTH, type=TAPER_TYPE, side=TAPER_SIDE) # Check that there are a minimum number of points in the noise window if noise.stats.npts < MIN_POINTS_IN_WINDOW: # Fail the trace, but still compute the signal spectra # ** only fail here if it hasn't already failed; we do not yet # ** support tracking multiple fail reasons and I think it is # ** better to know the FIRST reason if I have to pick one. if not tr.hasParameter('failure'): tr.fail('Failed SNR check; Not enough points in noise window.') compute_and_smooth_spectrum(tr, bandwidth, 'signal') return tr # Check that there are a minimum number of points in the noise window if signal.stats.npts < MIN_POINTS_IN_WINDOW: # Fail the trace, but still compute the signal spectra if not tr.hasParameter('failure'): tr.fail( 'Failed SNR check; Not enough points in signal window.') compute_and_smooth_spectrum(tr, bandwidth, 'signal') return tr nfft = max(next_pow_2(signal.stats.npts), next_pow_2(noise.stats.npts)) compute_and_smooth_spectrum(tr, bandwidth, 'noise', noise, nfft) compute_and_smooth_spectrum(tr, bandwidth, 'signal', signal, nfft) # For both the raw and smoothed spectra, subtract the noise spectrum # from the signal spectrum tr.setCached( 'signal_spectrum', { 'spec': tr.getCached('signal_spectrum')['spec'] - tr.getCached('noise_spectrum')['spec'], 'freq': tr.getCached('signal_spectrum')['freq'] }) tr.setCached( 'smooth_signal_spectrum', { 'spec': tr.getCached('smooth_signal_spectrum')['spec'] - tr.getCached('smooth_noise_spectrum')['spec'], 'freq': tr.getCached('smooth_signal_spectrum')['freq'] }) smooth_signal_spectrum = tr.getCached('smooth_signal_spectrum')['spec'] smooth_noise_spectrum = tr.getCached('smooth_noise_spectrum')['spec'] snr = smooth_signal_spectrum / smooth_noise_spectrum snr_dict = { 'snr': snr, 'freq': tr.getCached('smooth_signal_spectrum')['freq'] } tr.setCached('snr', snr_dict) else: # We do not have an estimate of the signal split time for this trace compute_and_smooth_spectrum(tr, bandwidth, 'signal') if check is not None: tr = snr_check(tr, mag, **check) return tr
def deconwater(uin, win, dt, tshift=10., wlevel=0.05, f0=2.0, normalize=False, phase='P'): """ Frequency-domain deconvolution using waterlevel method. :param uin: R or Q component for the response function :type uin: np.ndarray :param win: Z or L component for the source function :type win: np.ndarray :param dt: sample interval in second :type dt: float :param tshift: Time shift before P arrival, defaults to 10. :type tshift: float, optional :param wlevel: Waterlevel to stabilize the deconvolution, defaults to 0.05 :type wlevel: float, optional :param f0: Gauss factor, defaults to 2.0 :type f0: float, optional :param normalize: If normalize the amplitude of the RF, defaults to False :type normalize: bool, optional :return: (rf, rms) RF and final rms. :rtype: (np.ndarray, float) """ if uin.size != win.size: raise ValueError( 'The length of the \'uin\' must be same as the \'win\'') nt = uin.size nft = next_pow_2(nt) nfpts = nft / 2 + 1 # number of freq samples fny = 1. / (2. * dt) # nyquist delf = fny / (0.5 * nft) freq = delf * np.arange(nfpts) w = 2 * pi * freq # containers # rff = np.zeros(nft); # Rfn in freq domain upf = np.zeros(nft) # predicted numer in freq domain # Convert seismograms to freq domain uf = fft(uin, nft) wf = fft(win, nft) # denominator df = wf * wf.conjugate() dmax = max(df.real) # add water level correction phi1 = wlevel * dmax # water level # nwl = length( find(df<phi1) ) # number corrected df[np.where(df.real < phi1)[0]] = phi1 gaussF = gaussFilter(dt, nft, f0) nf = gaussF * uf * wf.conjugate() # compute RF rff = nf / df # compute predicted numerator upf = rff * wf # add phase shift to RF w = np.append(w, -np.flipud(w[1:-1])) rff = rff * np.exp(-1j * w * tshift) # back to time domain rft = ifft(rff, nft) rft = rft[0:nt].real # compute the fit uf = gaussF * uf # compare to filtered numerator ut = ifft(uf, nft).real upt = ifft(upf, nft).real powerU = np.sum(ut[0:nt]**2) rms = np.sum((upt[0:nt] - ut[0:nt])**2) / powerU if normalize: gnorm = np.sum(gaussF) * delf * dt rft = rft.real / gnorm return rft, rms
def array_processing(stream, win_len, win_frac, sll_x, slm_x, sll_y, slm_y, sl_s, semb_thres, vel_thres, frqlow, frqhigh, stime, etime, prewhiten, verbose=False, coordsys='lonlat', timestamp='mlabday', method=0, store=None, sl_corr=[0., 0.], normalize_waveforms=False): """ Method for Seismic-Array-Beamforming/FK-Analysis/Capon :param stream: Stream object, the trace.stats dict like class must contain an :class:`~obspy.core.util.attribdict.AttribDict` with 'latitude', 'longitude' (in degrees) and 'elevation' (in km), or 'x', 'y', 'elevation' (in km) items/attributes. See param ``coordsys``. :type win_len: float :param win_len: Sliding window length in seconds :type win_frac: float :param win_frac: Fraction of sliding window to use for step :type sll_x: float :param sll_x: slowness x min (lower) :type slm_x: float :param slm_x: slowness x max :type sll_y: float :param sll_y: slowness y min (lower) :type slm_y: float :param slm_y: slowness y max :type sl_s: float :param sl_s: slowness step :type semb_thres: float :param semb_thres: Threshold for semblance :type vel_thres: float :param vel_thres: Threshold for velocity :type frqlow: float :param frqlow: lower frequency for fk/capon :type frqhigh: float :param frqhigh: higher frequency for fk/capon :type stime: :class:`~obspy.core.utcdatetime.UTCDateTime` :param stime: Start time of interest :type etime: :class:`~obspy.core.utcdatetime.UTCDateTime` :param etime: End time of interest :type prewhiten: int :param prewhiten: Do prewhitening, values: 1 or 0 :param coordsys: valid values: 'lonlat' and 'xy', choose which stream attributes to use for coordinates :type timestamp: str :param timestamp: valid values: 'julsec' and 'mlabday'; 'julsec' returns the timestamp in seconds since 1970-01-01T00:00:00, 'mlabday' returns the timestamp in days (decimals represent hours, minutes and seconds) since '0001-01-01T00:00:00' as needed for matplotlib date plotting (see e.g. matplotlib's num2date) :type method: int :param method: the method to use 0 == bf, 1 == capon :type store: function :param store: A custom function which gets called on each iteration. It is called with the relative power map and the time offset as first and second arguments and the iteration number as third argument. Useful for storing or plotting the map for each iteration. For this purpose the dump function of this module can be used. :return: :class:`numpy.ndarray` of timestamp, relative relpow, absolute relpow, backazimuth, slowness """ res = [] eotr = True # check that sampling rates do not vary fs = stream[0].stats.sampling_rate if len(stream) != len(stream.select(sampling_rate=fs)): msg = 'in sonic sampling rates of traces in stream are not equal' raise ValueError(msg) grdpts_x = int(((slm_x - sll_x) / sl_s + 0.5) + 1) grdpts_y = int(((slm_y - sll_y) / sl_s + 0.5) + 1) geometry = get_geometry(stream, coordsys=coordsys, verbose=verbose) if verbose: print("geometry:") print(geometry) print("stream contains following traces:") print(stream) print("stime = " + str(stime) + ", etime = " + str(etime)) time_shift_table = get_timeshift(geometry, sll_x, sll_y, sl_s, grdpts_x, grdpts_y) # offset of arrays spoint, _epoint = get_spoint(stream, stime, etime) # # loop with a sliding window over the dat trace array and apply bbfk # nstat = len(stream) fs = stream[0].stats.sampling_rate nsamp = int(win_len * fs) nstep = int(nsamp * win_frac) # generate plan for rfftr nfft = next_pow_2(nsamp) deltaf = fs / float(nfft) nlow = int(frqlow / float(deltaf) + 0.5) nhigh = int(frqhigh / float(deltaf) + 0.5) nlow = max(1, nlow) # avoid using the offset nhigh = min(nfft // 2 - 1, nhigh) # avoid using nyquist nf = nhigh - nlow + 1 # include upper and lower frequency # to speed up the routine a bit we estimate all steering vectors in advance steer = np.empty((nf, grdpts_x, grdpts_y, nstat), dtype=np.complex128) clibsignal.calcSteer(nstat, grdpts_x, grdpts_y, nf, nlow, deltaf, time_shift_table, steer) _r = np.empty((nf, nstat, nstat), dtype=np.complex128) ft = np.empty((nstat, nf), dtype=np.complex128) newstart = stime # 0.22 matches 0.2 of historical C bbfk.c tap = cosine_taper(nsamp, p=0.22) offset = 0 relpow_map = np.empty((grdpts_x, grdpts_y), dtype=np.float64) abspow_map = np.empty((grdpts_x, grdpts_y), dtype=np.float64) while eotr: try: for i, tr in enumerate(stream): dat = tr.data[spoint[i] + offset:spoint[i] + offset + nsamp] dat = (dat - dat.mean()) * tap if normalize_waveforms: dat = dat / np.max(np.abs(dat)) ft[i, :] = np.fft.rfft(dat, nfft)[nlow:nlow + nf] except IndexError: break ft = np.ascontiguousarray(ft, np.complex128) relpow_map.fill(0.) abspow_map.fill(0.) # computing the covariances of the signal at different receivers dpow = 0. for i in range(nstat): for j in range(i, nstat): _r[:, i, j] = ft[i, :] * ft[j, :].conj() if method == 1: _r[:, i, j] /= np.abs(_r[:, i, j].sum()) if i != j: _r[:, j, i] = _r[:, i, j].conjugate() else: dpow += np.abs(_r[:, i, j].sum()) dpow *= nstat if method == 1: # P(f) = 1/(e.H R(f)^-1 e) for n in range(nf): _r[n, :, :] = np.linalg.pinv(_r[n, :, :], rcond=1e-6) errcode = clibsignal.generalizedBeamformer(relpow_map, abspow_map, steer, _r, nstat, prewhiten, grdpts_x, grdpts_y, nf, dpow, method) if errcode != 0: msg = 'generalizedBeamforming exited with error %d' raise Exception(msg % errcode) ix, iy = np.unravel_index(relpow_map.argmax(), relpow_map.shape) relpow, abspow = relpow_map[ix, iy], abspow_map[ix, iy] if store is not None: store(relpow_map, abspow_map, offset) # here we compute baz, slow slow_x = sll_x + ix * sl_s slow_y = sll_y + iy * sl_s # --------- slow_x = slow_x - sl_corr[0] slow_y = slow_y - sl_corr[1] # --------- slow = np.sqrt(slow_x**2 + slow_y**2) if slow < 1e-8: slow = 1e-8 azimut = 180 * math.atan2(slow_x, slow_y) / math.pi baz = azimut % -360 + 180 if relpow > semb_thres and 1. / slow > vel_thres: res.append( np.array([newstart.timestamp, relpow, abspow, baz, slow])) if verbose: print(newstart, (newstart + (nsamp / fs)), res[-1][1:]) if (newstart + (nsamp + nstep) / fs) > etime: eotr = False offset += nstep newstart += nstep / fs res = np.array(res) if timestamp == 'julsec': pass elif timestamp == 'mlabday': # 719163 == days between 1970 and 0001 + 1 res[:, 0] = res[:, 0] / (24. * 3600) + 719163 else: msg = "Option timestamp must be one of 'julsec', or 'mlabday'" raise ValueError(msg) return np.array(res)
def compute_snr_trace(tr, bandwidth, check=None): if tr.hasParameter('signal_split'): # Split the noise and signal into two separate traces split_prov = tr.getParameter('signal_split') if isinstance(split_prov, list): split_prov = split_prov[0] split_time = split_prov['split_time'] noise = tr.copy().trim(endtime=split_time) signal = tr.copy().trim(starttime=split_time) # Taper both windows noise.taper(max_percentage=TAPER_WIDTH, type=TAPER_TYPE, side=TAPER_SIDE) signal.taper(max_percentage=TAPER_WIDTH, type=TAPER_TYPE, side=TAPER_SIDE) # Check that there are a minimum number of points in the noise window if noise.stats.npts < MIN_POINTS_IN_WINDOW: # Fail the trace, but still compute the signal spectra # ** only fail here if it hasn't already failed; we do not yet # ** support tracking multiple fail reasons and I think it is # ** better to know the FIRST reason if I have to pick one. if not tr.hasParameter('failure'): tr.fail('Failed SNR check; Not enough points in noise window.') tr = compute_signal_spectrum(tr, bandwidth) return tr # Check that there are a minimum number of points in the noise window if signal.stats.npts < MIN_POINTS_IN_WINDOW: # Fail the trace, but still compute the signal spectra if not tr.hasParameter('failure'): tr.fail( 'Failed SNR check; Not enough points in signal window.') tr = compute_signal_spectrum(tr, bandwidth) return tr nfft = max(next_pow_2(signal.stats.npts), next_pow_2(noise.stats.npts)) # Transform to frequency domain and smooth spectra using # konno-ohmachi smoothing dt = signal.stats.delta sig_spec = abs(np.fft.rfft(signal.data, n=nfft)) * dt sig_spec_freqs = np.fft.rfftfreq(nfft, dt) dt = noise.stats.delta noise_spec = abs(np.fft.rfft(noise.data, n=nfft)) * dt sig_spec -= noise_spec sig_dict = { 'spec': sig_spec.tolist(), 'freq': sig_spec_freqs.tolist() } tr.setParameter('signal_spectrum', sig_dict) noise_dict = { 'spec': noise_spec.tolist(), 'freq': sig_spec_freqs.tolist() # same as signal } tr.setParameter('noise_spectrum', noise_dict) sig_spec_smooth, freqs_signal = fft_smooth( signal, nfft, bandwidth) smooth_dict = { 'spec': sig_spec_smooth.tolist(), 'freq': freqs_signal.tolist() } tr.setParameter('smooth_signal_spectrum', smooth_dict) noise_spec_smooth, freqs_noise = fft_smooth(noise, nfft) noise_smooth_dict = { 'spec': noise_spec_smooth.tolist(), 'freq': freqs_noise.tolist() } tr.setParameter('smooth_noise_spectrum', noise_smooth_dict) # remove the noise level from the spectrum of the signal window sig_spec_smooth -= noise_spec_smooth snr = sig_spec_smooth / noise_spec_smooth snr_dict = { 'snr': snr.tolist(), 'freq': freqs_signal.tolist() } tr.setParameter('snr', snr_dict) else: # We do not have an estimate of the signal split time for this trace tr = compute_signal_spectrum(tr, bandwidth) if check is not None: tr = snr_check(tr, **check) return tr
def array_processing(stream, win_len, win_frac, sll_x, slm_x, sll_y, slm_y, sl_s, semb_thres, vel_thres, frqlow, frqhigh, stime, etime, prewhiten, verbose=False, coordsys='lonlat', timestamp='mlabday', method=0, store=None): """ Method for Seismic-Array-Beamforming/FK-Analysis/Capon :param stream: Stream object, the trace.stats dict like class must contain an :class:`~obspy.core.util.attribdict.AttribDict` with 'latitude', 'longitude' (in degrees) and 'elevation' (in km), or 'x', 'y', 'elevation' (in km) items/attributes. See param ``coordsys``. :type win_len: float :param win_len: Sliding window length in seconds :type win_frac: float :param win_frac: Fraction of sliding window to use for step :type sll_x: float :param sll_x: slowness x min (lower) :type slm_x: float :param slm_x: slowness x max :type sll_y: float :param sll_y: slowness y min (lower) :type slm_y: float :param slm_y: slowness y max :type sl_s: float :param sl_s: slowness step :type semb_thres: float :param semb_thres: Threshold for semblance :type vel_thres: float :param vel_thres: Threshold for velocity :type frqlow: float :param frqlow: lower frequency for fk/capon :type frqhigh: float :param frqhigh: higher frequency for fk/capon :type stime: :class:`~obspy.core.utcdatetime.UTCDateTime` :param stime: Start time of interest :type etime: :class:`~obspy.core.utcdatetime.UTCDateTime` :param etime: End time of interest :type prewhiten: int :param prewhiten: Do prewhitening, values: 1 or 0 :param coordsys: valid values: 'lonlat' and 'xy', choose which stream attributes to use for coordinates :type timestamp: str :param timestamp: valid values: 'julsec' and 'mlabday'; 'julsec' returns the timestamp in seconds since 1970-01-01T00:00:00, 'mlabday' returns the timestamp in days (decimals represent hours, minutes and seconds) since '0001-01-01T00:00:00' as needed for matplotlib date plotting (see e.g. matplotlib's num2date) :type method: int :param method: the method to use 0 == bf, 1 == capon :type store: function :param store: A custom function which gets called on each iteration. It is called with the relative power map and the time offset as first and second arguments and the iteration number as third argument. Useful for storing or plotting the map for each iteration. For this purpose the dump function of this module can be used. :return: :class:`numpy.ndarray` of timestamp, relative relpow, absolute relpow, backazimuth, slowness """ res = [] eotr = True # check that sampling rates do not vary fs = stream[0].stats.sampling_rate if len(stream) != len(stream.select(sampling_rate=fs)): msg = 'in sonic sampling rates of traces in stream are not equal' raise ValueError(msg) grdpts_x = int(((slm_x - sll_x) / sl_s + 0.5) + 1) grdpts_y = int(((slm_y - sll_y) / sl_s + 0.5) + 1) geometry = get_geometry(stream, coordsys=coordsys, verbose=verbose) if verbose: print("geometry:") print(geometry) print("stream contains following traces:") print(stream) print("stime = " + str(stime) + ", etime = " + str(etime)) time_shift_table = get_timeshift(geometry, sll_x, sll_y, sl_s, grdpts_x, grdpts_y) # offset of arrays spoint, _epoint = get_spoint(stream, stime, etime) # # loop with a sliding window over the dat trace array and apply bbfk # nstat = len(stream) fs = stream[0].stats.sampling_rate nsamp = int(win_len * fs) nstep = int(nsamp * win_frac) # generate plan for rfftr nfft = next_pow_2(nsamp) deltaf = fs / float(nfft) nlow = int(frqlow / float(deltaf) + 0.5) nhigh = int(frqhigh / float(deltaf) + 0.5) nlow = max(1, nlow) # avoid using the offset nhigh = min(nfft // 2 - 1, nhigh) # avoid using nyquist nf = nhigh - nlow + 1 # include upper and lower frequency # to speed up the routine a bit we estimate all steering vectors in advance steer = np.empty((nf, grdpts_x, grdpts_y, nstat), dtype=np.complex128) clibsignal.calcSteer(nstat, grdpts_x, grdpts_y, nf, nlow, deltaf, time_shift_table, steer) _r = np.empty((nf, nstat, nstat), dtype=np.complex128) ft = np.empty((nstat, nf), dtype=np.complex128) newstart = stime # 0.22 matches 0.2 of historical C bbfk.c tap = cosine_taper(nsamp, p=0.22) offset = 0 relpow_map = np.empty((grdpts_x, grdpts_y), dtype=np.float64) abspow_map = np.empty((grdpts_x, grdpts_y), dtype=np.float64) while eotr: try: for i, tr in enumerate(stream): dat = tr.data[spoint[i] + offset: spoint[i] + offset + nsamp] dat = (dat - dat.mean()) * tap ft[i, :] = np.fft.rfft(dat, nfft)[nlow:nlow + nf] except IndexError: break ft = np.ascontiguousarray(ft, np.complex128) relpow_map.fill(0.) abspow_map.fill(0.) # computing the covariances of the signal at different receivers dpow = 0. for i in range(nstat): for j in range(i, nstat): _r[:, i, j] = ft[i, :] * ft[j, :].conj() if method == 1: _r[:, i, j] /= np.abs(_r[:, i, j].sum()) if i != j: _r[:, j, i] = _r[:, i, j].conjugate() else: dpow += np.abs(_r[:, i, j].sum()) dpow *= nstat if method == 1: # P(f) = 1/(e.H R(f)^-1 e) for n in range(nf): _r[n, :, :] = np.linalg.pinv(_r[n, :, :], rcond=1e-6) errcode = clibsignal.generalizedBeamformer( relpow_map, abspow_map, steer, _r, nstat, prewhiten, grdpts_x, grdpts_y, nf, dpow, method) if errcode != 0: msg = 'generalizedBeamforming exited with error %d' raise Exception(msg % errcode) ix, iy = np.unravel_index(relpow_map.argmax(), relpow_map.shape) relpow, abspow = relpow_map[ix, iy], abspow_map[ix, iy] if store is not None: store(relpow_map, abspow_map, offset) # here we compute baz, slow slow_x = sll_x + ix * sl_s slow_y = sll_y + iy * sl_s slow = np.sqrt(slow_x ** 2 + slow_y ** 2) if slow < 1e-8: slow = 1e-8 azimut = 180 * math.atan2(slow_x, slow_y) / math.pi baz = azimut % -360 + 180 if relpow > semb_thres and 1. / slow > vel_thres: res.append(np.array([newstart.timestamp, relpow, abspow, baz, slow])) if verbose: print(newstart, (newstart + (nsamp / fs)), res[-1][1:]) if (newstart + (nsamp + nstep) / fs) > etime: eotr = False offset += nstep newstart += nstep / fs res = np.array(res) if timestamp == 'julsec': pass elif timestamp == 'mlabday': # 719163 == days between 1970 and 0001 + 1 res[:, 0] = res[:, 0] / (24. * 3600) + 719163 else: msg = "Option timestamp must be one of 'julsec', or 'mlabday'" raise ValueError(msg) return np.array(res)
wdata = np.real(scipy.fftpack.ifft(FFTRawSign)) plt.subplot(414) plt.plot(np.arange(len(wdata)) * delta, wdata) plt.xlim(0, nt * delta) plt.title("Output trace after Whiten and filter") plt.show() return FFTRawSign if __name__ == '__main__': import time a = obspy.read('IC.BJT.00.BHZ.norm')[0] nt = a.stats.npts N = next_pow_2(nt) # a = np.sin(a) + np.sin(a / 4.) + np.sin(a / 16.) # a -= a.mean() t = time.clock() fftdata = whiten(a.copy(), nt, N, a.stats.delta, 0.01, 0.02, 0.067, 0.08, plot=True) print("whiten the trace:", (time.clock()-t) * 1000, "ms") ''' wdata = np.real(scipy.fftpack.ifft(fftdata)) wdata = wdata[0:nt]/smooth(np.abs(wdata[0:nt]), half_len=20) plt.plot(np.arange(len(wdata)) * a.stats.delta, wdata) plt.xlim(0, nt*a.stats.delta) plt.show() '''
def simulate_seismometer(data, samp_rate, paz_remove=None, paz_simulate=None, remove_sensitivity=True, simulate_sensitivity=True, water_level=600.0, zero_mean=True, taper=True, taper_fraction=0.05, pre_filt=None, seedresp=None, nfft_pow2=False, pitsasim=True, sacsim=False, shsim=False): """ Simulate/Correct seismometer. :type data: NumPy :class:`~numpy.ndarray` :param data: Seismogram, detrend before hand (e.g. zero mean) :type samp_rate: float :param samp_rate: Sample Rate of Seismogram :type paz_remove: dict, None :param paz_remove: Dictionary containing keys 'poles', 'zeros', 'gain' (A0 normalization factor). poles and zeros must be a list of complex floating point numbers, gain must be of type float. Poles and Zeros are assumed to correct to m/s, SEED convention. Use None for no inverse filtering. :type paz_simulate: dict, None :param paz_simulate: Dictionary containing keys 'poles', 'zeros', 'gain'. Poles and zeros must be a list of complex floating point numbers, gain must be of type float. Or None for no simulation. :type remove_sensitivity: bool :param remove_sensitivity: Determines if data is divided by `paz_remove['sensitivity']` to correct for overall sensitivity of recording instrument (seismometer/digitizer) during instrument correction. :type simulate_sensitivity: bool :param simulate_sensitivity: Determines if data is multiplied with `paz_simulate['sensitivity']` to simulate overall sensitivity of new instrument (seismometer/digitizer) during instrument simulation. :type water_level: float :param water_level: Water_Level for spectrum to simulate :type zero_mean: bool :param zero_mean: If true the mean of the data is subtracted :type taper: bool :param taper: If true a cosine taper is applied. :type taper_fraction: float :param taper_fraction: Taper fraction of cosine taper to use :type pre_filt: list or tuple of floats :param pre_filt: Apply a bandpass filter to the data trace before deconvolution. The list or tuple defines the four corner frequencies (f1,f2,f3,f4) of a cosine taper which is one between f2 and f3 and tapers to zero for f1 < f < f2 and f3 < f < f4. :type seedresp: dict, None :param seedresp: Dictionary contains keys 'filename', 'date', 'units'. 'filename' is the path to a RESP-file generated from a dataless SEED volume (or a file like object with RESP information); 'date' is a :class:`~obspy.core.utcdatetime.UTCDateTime` object for the date that the response function should be extracted for (can be omitted when calling simulate() on Trace/Stream. the Trace's starttime will then be used); 'units' defines the units of the response function. Can be either 'DIS', 'VEL' or 'ACC'. :type nfft_pow2: bool :param nfft_pow2: Number of frequency points to use for FFT. If True, the exact power of two is taken (default in PITSA). If False the data are not zero-padded to the next power of two which makes a slower FFT but is then much faster for e.g. evalresp which scales with the FFT points. :type pitsasim: bool :param pitsasim: Choose parameters to match instrument correction as done by PITSA. :type sacsim: bool :param sacsim: Choose parameters to match instrument correction as done by SAC. :type shsim: bool :param shsim: Choose parameters to match instrument correction as done by Seismic Handler. :return: The corrected data are returned as :class:`numpy.ndarray` float64 array. float64 is chosen to avoid numerical instabilities. This function works in the frequency domain, where nfft is the next power of len(data) to avoid wrap around effects during convolution. The inverse of the frequency response of the seismometer (``paz_remove``) is convolved with the spectrum of the data and with the frequency response of the seismometer to simulate (``paz_simulate``). A 5% cosine taper is taken before simulation. The data must be detrended (e.g.) zero mean beforehand. If paz_simulate=None only the instrument correction is done. In the latter case, a broadband filter can be applied to the data trace using pre_filt. This restricts the signal to the valid frequency band and thereby avoids artifacts due to amplification of frequencies outside of the instrument's passband (for a detailed discussion see *Of Poles and Zeros*, F. Scherbaum, Kluwer Academic Publishers). .. versionchanged:: 0.5.1 The default for `remove_sensitivity` and `simulate_sensitivity` has been changed to ``True``. Old deprecated keyword arguments `paz`, `inst_sim`, `no_inverse_filtering` have been removed. """ # Checking the types if not paz_remove and not paz_simulate and not seedresp: msg = "Neither inverse nor forward instrument simulation specified." raise TypeError(msg) for d in [paz_remove, paz_simulate]: if d is None: continue for key in ['poles', 'zeros', 'gain']: if key not in d: raise KeyError("Missing key: %s" % key) # Translated from PITSA: spr_resg.c delta = 1.0 / samp_rate # ndat = len(data) data = data.astype(np.float64) if zero_mean: data -= data.mean() if taper: if sacsim: data *= cosine_taper(ndat, taper_fraction, sactaper=sacsim, halfcosine=False) else: data *= cosine_taper(ndat, taper_fraction) # The number of points for the FFT has to be at least 2 * ndat (in # order to prohibit wrap around effects during convolution) cf. # Numerical Recipes p. 429 calculate next power of 2. if nfft_pow2: nfft = util.next_pow_2(2 * ndat) # evalresp scales directly with nfft, therefore taking the next power of # two has a greater negative performance impact than the slow down of a # not power of two in the FFT else: nfft = _npts2nfft(ndat) # Transform data in Fourier domain data = np.fft.rfft(data, n=nfft) # Inverse filtering = Instrument correction if paz_remove: freq_response, freqs = paz_to_freq_resp(paz_remove['poles'], paz_remove['zeros'], paz_remove['gain'], delta, nfft, freq=True) if seedresp: freq_response, freqs = evalresp(delta, nfft, seedresp['filename'], seedresp['date'], units=seedresp['units'], freq=True, network=seedresp['network'], station=seedresp['station'], locid=seedresp['location'], channel=seedresp['channel']) if not remove_sensitivity: msg = "remove_sensitivity is set to False, but since seedresp " + \ "is selected the overall sensitivity will be corrected " + \ " for anyway!" warnings.warn(msg) if paz_remove or seedresp: if pre_filt: # make cosine taper fl1, fl2, fl3, fl4 = pre_filt if sacsim: cos_win = cosine_sac_taper(freqs, flimit=(fl1, fl2, fl3, fl4)) else: cos_win = cosine_taper(freqs.size, freqs=freqs, flimit=(fl1, fl2, fl3, fl4)) data *= cos_win invert_spectrum(freq_response, water_level) data *= freq_response del freq_response # Forward filtering = Instrument simulation if paz_simulate: data *= paz_to_freq_resp(paz_simulate['poles'], paz_simulate['zeros'], paz_simulate['gain'], delta, nfft) data[-1] = abs(data[-1]) + 0.0j # transform data back into the time domain data = np.fft.irfft(data)[0:ndat] if pitsasim: # linear detrend data = simple_detrend(data) if shsim: # detrend using least squares data = scipy.signal.detrend(data, type="linear") # correct for involved overall sensitivities if paz_remove and remove_sensitivity and not seedresp: data /= paz_remove['sensitivity'] if paz_simulate and simulate_sensitivity: data *= paz_simulate['sensitivity'] return data
def deconvf(rsp_list, src, sampling_rate, waterlevel=0.05, gauss=2., tshift=10., pad=0, length=None, normalize=0, return_info=False): """ Frequency-domain deconvolution using waterlevel method. Deconvolve src from arrays in rsp_list. :param rsp_list: either a list of arrays containing the response functions or a single array :param src: array with source function :param sampling_rate: sampling rate of the data :param waterlevel: waterlevel to stabilize the deconvolution :param gauss: Gauss parameter of Low-pass filter :param tshift: delay time 0s will be at time tshift afterwards :param pad: multiply number of samples used for fft by 2**pad :param length: number of data points in results, optional :param normalize: normalize all results so that the maximum of the trace with supplied index is 1. Set normalize to 'src' to normalize for the maximum of the prepared source. Set normalize to None for no normalization. :param return_info: return additionally a lot of different parameters in a dict for debugging purposes :return: (list of) array(s) with deconvolution(s) """ if length is None: length = __get_length(rsp_list) N = length nfft = next_pow_2(N) * 2**pad freq = np.fft.fftfreq(nfft, d=1. / sampling_rate) gauss = np.exp( np.maximum(-(0.5 * 2 * pi * freq / gauss)**2, -700.) - 1j * tshift * 2 * pi * freq) spec_src = fft(src, nfft) spec_src_conj = np.conjugate(spec_src) spec_src_water = np.abs(spec_src * spec_src_conj) spec_src_water = np.maximum(spec_src_water, max(spec_src_water) * waterlevel) if normalize == 'src': spec_src = gauss * spec_src * spec_src_conj / spec_src_water rf_src = ifft(spec_src, nfft)[:N] norm = 1 / max(rf_src) rf_src = norm * rf_src flag = False if not isinstance(rsp_list, (list, tuple)): flag = True rsp_list = [rsp_list] rf_list = [ ifft(gauss * fft(rsp, nfft) * spec_src_conj / spec_src_water, nfft)[:N] for rsp in rsp_list ] if normalize not in (None, 'src'): norm = 1. / max(rf_list[normalize]) if normalize is not None: for rf in rf_list: rf *= norm if return_info: if normalize not in (None, 'src'): spec_src = gauss * spec_src * spec_src_conj / spec_src_water rf_src = ifft(spec_src, nfft)[:N] norm = 1 / max(rf_src) rf_src = norm * rf_src info = { 'rf_src': rf_src, 'rf_src_conj': spec_src_conj, 'spec_src_water': spec_src_water, 'freq': freq, 'gauss': gauss, 'norm': norm, 'N': N, 'nfft': nfft } return rf_list, info elif flag: return rf else: return rf_list
def decovit(uin, win, dt, nt=None, tshift=10, f0=2.0, itmax=400, minderr=0.001, info=False): """ Created on Wed Sep 10 14:21:38 2014 [RFI, rms, it]=makeRFitdecon(uin,win,dt,nt,tshift,f0,itmax,minderr) In: uin = numerator (radial for PdS) win = denominator (vertical component for PdS) dt = sample interval (s) nt = number of samples tshift = Time until beginning of receiver function (s) f0 = width of gaussian filter itmax = max # iterations minderr = Min change in error required for stopping iterations Out: RFI = receiver function rms = Root mean square error for predicting numerator after each iteration @author: Mijian Xu @ NJU """ # print('Iterative Decon (Ligorria & Ammon):\n') if len(uin) != len(win): raise ValueError('The two input trace must be in same length') elif nt is None: nt = len(uin) else: pass rms = np.zeros(itmax) nfft = next_pow_2(nt) p0 = np.zeros(nfft) u0 = np.zeros(nfft) w0 = np.zeros(nfft) u0[0:nt] = uin w0[0:nt] = win gaussF = gaussFilter(dt, nfft, f0) u_flt = gfilter(u0, nfft, gaussF, dt) w_flt = gfilter(w0, nfft, gaussF, dt) wf = fft(w0, nfft) r_flt = u_flt powerU = np.sum(u_flt**2) it = 0 sumsq_i = 1 d_error = 100 * powerU + minderr maxlag = 0.5 * nfft # print('\tMax Spike Display is ' + str((maxlag) * dt)) while np.abs(d_error) > minderr and it < itmax: rw = correl(r_flt, w_flt, nfft) rw = rw / np.sum(w_flt**2) i1 = np.argmax(np.abs(rw[0:int(maxlag) - 1])) amp = rw[i1] / dt p0[i1] = p0[i1] + amp p_flt = gfilter(p0, nfft, gaussF, dt) p_flt = gfilter(p_flt, nfft, wf, dt) r_flt = u_flt - p_flt sumsq = np.sum(r_flt**2) / powerU rms[it] = sumsq d_error = 100 * (sumsq_i - sumsq) sumsq_i = sumsq it = it + 1 p_flt = gfilter(p0, nfft, gaussF, dt) p_flt = phaseshift(p_flt, nfft, dt, tshift) RFI = p_flt[0:nt] rms = rms[0:it - 1] return RFI, rms, it
def compute_snr_trace(tr, bandwidth, mag=None, check=None): if tr.hasParameter("signal_split"): # Split the noise and signal into two separate traces split_prov = tr.getParameter("signal_split") if isinstance(split_prov, list): split_prov = split_prov[0] split_time = split_prov["split_time"] noise = tr.copy().trim(endtime=split_time) signal = tr.copy().trim(starttime=split_time) tr.setCached("noise_trace", { "times": noise.times(), "data": noise.data }) noise.detrend("demean") signal.detrend("demean") # Taper both windows noise.taper(max_percentage=TAPER_WIDTH, type=TAPER_TYPE, side=TAPER_SIDE) signal.taper(max_percentage=TAPER_WIDTH, type=TAPER_TYPE, side=TAPER_SIDE) # Check that there are a minimum number of points in the noise window if noise.stats.npts < MIN_POINTS_IN_WINDOW: # Fail the trace, but still compute the signal spectra # ** only fail here if it hasn't already failed; we do not yet # ** support tracking multiple fail reasons and I think it is # ** better to know the FIRST reason if I have to pick one. if not tr.hasParameter("failure"): tr.fail("Failed SNR check; Not enough points in noise window.") compute_and_smooth_spectrum(tr, bandwidth, "signal") return tr # Check that there are a minimum number of points in the noise window if signal.stats.npts < MIN_POINTS_IN_WINDOW: # Fail the trace, but still compute the signal spectra if not tr.hasParameter("failure"): tr.fail( "Failed SNR check; Not enough points in signal window.") compute_and_smooth_spectrum(tr, bandwidth, "signal") return tr nfft = max(next_pow_2(signal.stats.npts), next_pow_2(noise.stats.npts)) compute_and_smooth_spectrum(tr, bandwidth, "noise", noise, nfft) compute_and_smooth_spectrum(tr, bandwidth, "signal", signal, nfft) # For both the raw and smoothed spectra, subtract the noise spectrum # from the signal spectrum tr.setCached( "signal_spectrum", { "spec": (tr.getCached("signal_spectrum")["spec"] - tr.getCached("noise_spectrum")["spec"]), "freq": tr.getCached("signal_spectrum")["freq"], }, ) tr.setCached( "smooth_signal_spectrum", { "spec": (tr.getCached("smooth_signal_spectrum")["spec"] - tr.getCached("smooth_noise_spectrum")["spec"]), "freq": tr.getCached("smooth_signal_spectrum")["freq"], }, ) smooth_signal_spectrum = tr.getCached("smooth_signal_spectrum")["spec"] smooth_noise_spectrum = tr.getCached("smooth_noise_spectrum")["spec"] snr = smooth_signal_spectrum / smooth_noise_spectrum snr_dict = { "snr": snr, "freq": tr.getCached("smooth_signal_spectrum")["freq"] } tr.setCached("snr", snr_dict) else: # We do not have an estimate of the signal split time for this trace compute_and_smooth_spectrum(tr, bandwidth, "signal") if check is not None: tr = snr_check(tr, mag, **check) return tr
def rel_calib_stack(st1, st2, calib_file, window_len, overlap_frac=0.5, smooth=0, save_data=True): """ Method for relative calibration of sensors using a sensor with known transfer function :param st1: Stream or Trace object, (known) :param st2: Stream or Trace object, (unknown) :type calib_file: str :param calib_file: file name of calibration file containing the PAZ of the known instrument in GSE2 standard. :type window_len: float :param window_len: length of sliding window in seconds :type overlap_frac: float :param overlap_frac: fraction of overlap, defaults to fifty percent (0.5) :type smooth: float :param smooth: variable that defines if the Konno-Ohmachi taper is used or not. default = 0 -> no taper generally used in geopsy: smooth = 40 :type save_data: bool :param save_data: Whether or not to save the result to a file. If True, two output files will be created: * The new response in station_name.window_length.resp * The ref response in station_name.refResp Defaults to True :returns: frequency, amplitude and phase spectrum implemented after rel_calib_stack.c by M.Ohrnberger and J.Wassermann. """ # transform given trace objects to streams if isinstance(st1, Trace): st1 = Stream([st1]) if isinstance(st2, Trace): st2 = Stream([st2]) # check if sampling rate and trace length is the same if st1[0].stats.npts != st2[0].stats.npts: msg = "Traces don't have the same length!" raise ValueError(msg) elif st1[0].stats.sampling_rate != st2[0].stats.sampling_rate: msg = "Traces don't have the same sampling rate!" raise ValueError(msg) else: ndat1 = st1[0].stats.npts sampfreq = st1[0].stats.sampling_rate # read waveforms tr1 = st1[0].data.astype(np.float64) tr2 = st2[0].data.astype(np.float64) # get window length, nfft and frequency step ndat = int(window_len * sampfreq) nfft = next_pow_2(ndat) # read calib file and calculate response function gg, _freq = _calc_resp(calib_file, nfft, sampfreq) # calculate number of windows and overlap nwin = int(np.floor((ndat1 - nfft) / (nfft / 2)) + 1) noverlap = nfft * overlap_frac auto, _freq, _t = spectral_helper(tr1, tr1, NFFT=nfft, Fs=sampfreq, noverlap=noverlap) cross, freq, _t = spectral_helper(tr2, tr1, NFFT=nfft, Fs=sampfreq, noverlap=noverlap) res = (cross / auto).sum(axis=1) * gg # The first item might be zero. Problems with phase calculations. res = res[1:] freq = freq[1:] gg = gg[1:] res /= nwin # apply Konno-Ohmachi smoothing taper if chosen if smooth > 0: # Write in one matrix for performance reasons. spectra = np.empty((2, len(res.real))) spectra[0] = res.real spectra[1] = res.imag new_spectra = konno_ohmachi_smoothing( spectra, freq, bandwidth=smooth, count=1, max_memory_usage=1024, normalize=True ) res.real = new_spectra[0] res.imag = new_spectra[1] amp = np.abs(res) # include phase unwrapping phase = np.unwrap(np.angle(res)) # + 2.0 * np.pi ra = np.abs(gg) rpha = np.unwrap(np.angle(gg)) if save_data: trans_new = st2[0].stats.station + "." + st2[0].stats.channel + "." + str(window_len) + ".resp" trans_ref = st1[0].stats.station + ".refResp" # Create empty array for easy saving temp = np.empty((len(freq), 3)) temp[:, 0] = freq temp[:, 1] = amp temp[:, 2] = phase np.savetxt(trans_new, temp, fmt=native_str("%.10f")) temp[:, 1] = ra temp[:, 2] = rpha np.savetxt(trans_ref, temp, fmt=native_str("%.10f")) return freq, amp, phase
def _get_info(self): """ Returns a dictionary with information about the currently loaded database. """ # Get the size of all netCDF files. filesize = 0 for m in self.meshes: if m: filesize += os.path.getsize(m.filename) if self._is_reciprocal: if hasattr(self.meshes, "merged"): # The number of dimensions determines the available components. dims = self.meshes.merged.f["MergedSnapshots"].shape[1] if dims == 5: components = 'vertical and horizontal' elif dims == 3: components = 'horizontal only' elif dims == 2: components = 'vertical only' else: # pragma: no cover raise NotImplementedError elif self.meshes.pz is not None and self.meshes.px is not None: components = 'vertical and horizontal' elif self.meshes.pz is None and self.meshes.px is not None: components = 'horizontal only' elif self.meshes.pz is not None and self.meshes.px is None: components = 'vertical only' else: components = '4 elemental moment tensors' return dict( is_reciprocal=self._is_reciprocal, components=components, source_depth=float(self.parsed_mesh.source_depth) if self._is_reciprocal is False else None, velocity_model=self.parsed_mesh.background_model, external_model_name=self.parsed_mesh.external_model_name, attenuation=self.parsed_mesh.attenuation, period=float(self.parsed_mesh.dominant_period), dump_type=self.parsed_mesh.dump_type, excitation_type=self.parsed_mesh.excitation_type, dt=float(self.parsed_mesh.dt), sampling_rate=float(1.0 / self.parsed_mesh.dt), npts=int(self.parsed_mesh.ndumps), nfft=int(next_pow_2(self.parsed_mesh.ndumps) * 2), length=float(self.parsed_mesh.dt * (self.parsed_mesh.ndumps - 1)), stf=self.parsed_mesh.stf_kind, src_shift=float(self.parsed_mesh.source_shift), src_shift_samples=int(self.parsed_mesh.source_shift_samp), slip=self.parsed_mesh.stf_norm, sliprate=self.parsed_mesh.stf_d_norm, spatial_order=int(self.parsed_mesh.npol), min_radius=float(self.parsed_mesh.kwf_rmin) * 1e3, max_radius=float(self.parsed_mesh.kwf_rmax) * 1e3, planet_radius=float(self.parsed_mesh.planet_radius), min_d=float(self.parsed_mesh.kwf_colatmin), max_d=float(self.parsed_mesh.kwf_colatmax), time_scheme=self.parsed_mesh.time_scheme, directory=os.path.relpath(self.db_path), filesize=filesize, compiler=self.parsed_mesh.axisem_compiler, user=self.parsed_mesh.axisem_user, format_version=int(self.parsed_mesh.file_version), axisem_version=self.parsed_mesh.axisem_version, datetime=self.parsed_mesh.creation_time )
def deconvf(rsp_list, src, sampling_rate, waterlevel=0.05, gauss=2., tshift=10., pad=0, length=None, normalize=0, return_info=False): """ Frequency-domain deconvolution using waterlevel method. Deconvolve src from arrays in rsp_list. :param rsp_list: either a list of arrays containing the response functions or a single array :param src: array with source function :param sampling_rate: sampling rate of the data :param waterlevel: waterlevel to stabilize the deconvolution :param gauss: Gauss parameter of Low-pass filter :param tshift: delay time 0s will be at time tshift afterwards :param pad: multiply number of samples used for fft by 2**pad :param length: number of data points in results, optional :param normalize: normalize all results so that the maximum of the trace with supplied index is 1. Set normalize to 'src' to normalize for the maximum of the prepared source. Set normalize to None for no normalization. :param return_info: return additionally a lot of different parameters in a dict for debugging purposes :return: (list of) array(s) with deconvolution(s) """ if length is None: length = __get_length(rsp_list) N = length nfft = next_pow_2(N) * 2 ** pad freq = np.fft.fftfreq(nfft, d=1. / sampling_rate) gauss = np.exp(np.maximum(-(0.5 * 2 * pi * freq / gauss) ** 2, -700.) - 1j * tshift * 2 * pi * freq) spec_src = fft(src, nfft) spec_src_conj = np.conjugate(spec_src) spec_src_water = np.abs(spec_src * spec_src_conj) spec_src_water = np.maximum( spec_src_water, max(spec_src_water) * waterlevel) if normalize == 'src': spec_src = gauss * spec_src * spec_src_conj / spec_src_water rf_src = ifft(spec_src, nfft)[:N] norm = 1 / max(rf_src) rf_src = norm * rf_src flag = False if not isinstance(rsp_list, (list, tuple)): flag = True rsp_list = [rsp_list] rf_list = [ifft(gauss * fft(rsp, nfft) * spec_src_conj / spec_src_water, nfft)[:N] for rsp in rsp_list] if normalize not in (None, 'src'): norm = 1. / max(rf_list[normalize]) if normalize is not None: for rf in rf_list: rf *= norm if return_info: if normalize not in (None, 'src'): spec_src = gauss * spec_src * spec_src_conj / spec_src_water rf_src = ifft(spec_src, nfft)[:N] norm = 1 / max(rf_src) rf_src = norm * rf_src info = {'rf_src': rf_src, 'rf_src_conj': spec_src_conj, 'spec_src_water': spec_src_water, 'freq': freq, 'gauss': gauss, 'norm': norm, 'N': N, 'nfft': nfft} return rf_list, info elif flag: return rf else: return rf_list
def get_corner_frequencies(trace, event_time, epi_dist, ratio=3.0, split_method='velocity', vsplit=7.0, max_low_corner=0.1, min_high_corner=5.0, taper_type='hann', taper_percentage=0.05, taper_side='both'): """ Returns the corner frequencies for a trace. Args: trace (obspy.core.trace.Trace): Trace of strong motion data. event_time (UTCDateTime): Event origin time. epi_dist (float): Distance from event epicenter to station. split_method (str): Method for splitting signal and noise. Either 'p_arrival' or 'velocity' vsplit (float): Velocity (km/s) for splitting noise and signal. ratio (float): Required signal-to-noise ratio. Default is 3.0. max_low_corner (float): Maxmimum low corner frequency allowed. Default is 0.1. min_high_corner(float): Minimum low corner frequency allowed. Default is 5.0. taper_type (str): Taper types allowed for filtering. Must be an Obspy supported tapering method. Default is 'hann' (Hanning taper). taper_percentage (float): Decimal percentage of taper. Default is 0.05 (5%). taper_side (str): Speicfy which sides should be tapered. Either 'left', 'right', or 'both'. Default is 'both'. Returns: list : List of floats representing corner frequencies. Returns two -1 values if inadequate signal to noise ratio. """ # Split the noise and signal into two separate traces signal, noise = split_signal_and_noise(trace, event_time, epi_dist, split_method, vsplit) # Check if signal and noise splitting failed if (signal == -1 and noise == -1): return [-1, -1] # Taper the noise and signal traces noise = taper(noise, taper_type=taper_type, max_percentage=taper_percentage, side=taper_side) signal = taper(signal, taper_type=taper_type, max_percentage=taper_percentage, side=taper_side) # Find the number of points for the Fourier transform nfft = max(next_pow_2(signal.stats.npts), next_pow_2(noise.stats.npts)) # Transform to frequency domain and smooth spectra using # konno-ohmachi smoothing sig_spec_smooth, freqs_signal = fft_smooth(signal, nfft) noise_spec_smooth, freqs_noise = fft_smooth(noise, nfft) # remove the noise level from the spectrum of the signal window sig_spec_smooth -= noise_spec_smooth # Loop through frequencies to find low corner and high corner corner_frequencies = [] lows = [] highs = [] have_low = False for idx, freq in enumerate(freqs_signal): if have_low is False: if (sig_spec_smooth[idx] / noise_spec_smooth[idx]) >= ratio: lows.append(freq) have_low = True else: continue else: if (sig_spec_smooth[idx] / noise_spec_smooth[idx]) < ratio: highs.append(freq) have_low = False else: continue # If we didn't find any corners if not lows: return [-2, -2] # If we find an extra low, add another high for the maximum frequency if len(lows) > len(highs): highs.append(max(freqs_signal)) # Check if any of the low/high pairs are valid found_valid = False for idx, val in enumerate(lows): if (val <= max_low_corner and highs[idx] > min_high_corner): low_corner = val high_corner = highs[idx] found_valid = True # Check if we found any valid pairs if not found_valid: return [-3, -3] else: corner_frequencies = [low_corner, high_corner] corners = { 'get_dynamically': True, 'ratio': ratio, 'default_high_frequency': corner_frequencies[1], 'default_low_frequency': corner_frequencies[0] } # retrieving corner frequencies does not alter the data # trace = _update_params(trace, 'corners', corners) return corner_frequencies
def _get_info(self): """ Returns a dictionary with information about the currently loaded database. """ # Get the size of all netCDF files. filesize = 0 for m in self.meshes: if m: filesize += os.path.getsize(m.filename) if self._is_reciprocal: if hasattr(self.meshes, "merged"): # The number of dimensions determines the available components. dims = self.meshes.merged.f["MergedSnapshots"].shape[1] if dims == 5: components = 'vertical and horizontal' elif dims == 3: components = 'horizontal only' elif dims == 2: components = 'vertical only' else: # pragma: no cover raise NotImplementedError elif self.meshes.pz is not None and self.meshes.px is not None: components = 'vertical and horizontal' elif self.meshes.pz is None and self.meshes.px is not None: components = 'horizontal only' elif self.meshes.pz is not None and self.meshes.px is None: components = 'vertical only' else: components = '4 elemental moment tensors' return dict(is_reciprocal=self._is_reciprocal, components=components, source_depth=float(self.parsed_mesh.source_depth) if self._is_reciprocal is False else None, velocity_model=self.parsed_mesh.background_model, external_model_name=self.parsed_mesh.external_model_name, attenuation=self.parsed_mesh.attenuation, period=float(self.parsed_mesh.dominant_period), dump_type=self.parsed_mesh.dump_type, excitation_type=self.parsed_mesh.excitation_type, dt=float(self.parsed_mesh.dt), sampling_rate=float(1.0 / self.parsed_mesh.dt), npts=int(self.parsed_mesh.ndumps), nfft=int(next_pow_2(self.parsed_mesh.ndumps) * 2), length=float(self.parsed_mesh.dt * (self.parsed_mesh.ndumps - 1)), stf=self.parsed_mesh.stf_kind, src_shift=float(self.parsed_mesh.source_shift), src_shift_samples=int(self.parsed_mesh.source_shift_samp), slip=self.parsed_mesh.stf_norm, sliprate=self.parsed_mesh.stf_d_norm, spatial_order=int(self.parsed_mesh.npol), min_radius=float(self.parsed_mesh.kwf_rmin) * 1e3, max_radius=float(self.parsed_mesh.kwf_rmax) * 1e3, planet_radius=float(self.parsed_mesh.planet_radius), min_d=float(self.parsed_mesh.kwf_colatmin), max_d=float(self.parsed_mesh.kwf_colatmax), time_scheme=self.parsed_mesh.time_scheme, directory=os.path.relpath(self.db_path), filesize=filesize, compiler=self.parsed_mesh.axisem_compiler, user=self.parsed_mesh.axisem_user, format_version=int(self.parsed_mesh.file_version), axisem_version=self.parsed_mesh.axisem_version, datetime=self.parsed_mesh.creation_time)
def rel_calib_stack(st1, st2, calib_file, window_len, overlap_frac=0.5, smooth=0, save_data=True): """ Method for relative calibration of sensors using a sensor with known transfer function :param st1: Stream or Trace object, (known) :param st2: Stream or Trace object, (unknown) :type calib_file: str or dict :param calib_file: file name of calibration file containing the PAZ of the known instrument in GSE2 standard or a dictionary with poles and zeros information (with keys ``'poles'``, ``'zeros'`` and ``'sensitivity'``). :type window_len: float :param window_len: length of sliding window in seconds :type overlap_frac: float :param overlap_frac: fraction of overlap, defaults to fifty percent (0.5) :type smooth: float :param smooth: variable that defines if the Konno-Ohmachi taper is used or not. default = 0 -> no taper generally used in geopsy: smooth = 40 :type save_data: bool :param save_data: Whether or not to save the result to a file. If True, two output files will be created: * The new response in station_name.window_length.resp * The ref response in station_name.refResp Defaults to True :returns: frequency, amplitude and phase spectrum implemented after rel_calib_stack.c by M.Ohrnberger and J.Wassermann. """ # transform given trace objects to streams if isinstance(st1, Trace): st1 = Stream([st1]) if isinstance(st2, Trace): st2 = Stream([st2]) # check if sampling rate and trace length is the same if st1[0].stats.npts != st2[0].stats.npts: msg = "Traces don't have the same length!" raise ValueError(msg) elif st1[0].stats.sampling_rate != st2[0].stats.sampling_rate: msg = "Traces don't have the same sampling rate!" raise ValueError(msg) else: ndat1 = st1[0].stats.npts sampfreq = st1[0].stats.sampling_rate # read waveforms tr1 = st1[0].data.astype(np.float64) tr2 = st2[0].data.astype(np.float64) # get window length, nfft and frequency step ndat = int(window_len * sampfreq) nfft = next_pow_2(ndat) # read calib file and calculate response function gg, _freq = _calc_resp(calib_file, nfft, sampfreq) # calculate number of windows and overlap nwin = int(np.floor((ndat1 - nfft) / (nfft / 2)) + 1) noverlap = nfft * overlap_frac auto, _freq, _t = \ spectral_helper(tr1, tr1, NFFT=nfft, Fs=sampfreq, noverlap=noverlap) cross, freq, _t = \ spectral_helper(tr2, tr1, NFFT=nfft, Fs=sampfreq, noverlap=noverlap) res = (cross / auto).sum(axis=1) * gg # The first item might be zero. Problems with phase calculations. res = res[1:] freq = freq[1:] gg = gg[1:] res /= nwin # apply Konno-Ohmachi smoothing taper if chosen if smooth > 0: # Write in one matrix for performance reasons. spectra = np.empty((2, len(res.real))) spectra[0] = res.real spectra[1] = res.imag new_spectra = \ konno_ohmachi_smoothing(spectra, freq, bandwidth=smooth, count=1, max_memory_usage=1024, normalize=True) res.real = new_spectra[0] res.imag = new_spectra[1] amp = np.abs(res) # include phase unwrapping phase = np.unwrap(np.angle(res)) # + 2.0 * np.pi ra = np.abs(gg) rpha = np.unwrap(np.angle(gg)) if save_data: trans_new = (st2[0].stats.station + "." + st2[0].stats.channel + "." + str(window_len) + ".resp") trans_ref = st1[0].stats.station + ".refResp" # Create empty array for easy saving temp = np.empty((len(freq), 3)) temp[:, 0] = freq temp[:, 1] = amp temp[:, 2] = phase np.savetxt(trans_new, temp, fmt=native_str('%.10f')) temp[:, 1] = ra temp[:, 2] = rpha np.savetxt(trans_ref, temp, fmt=native_str('%.10f')) return freq, amp, phase
def plot_tfr(st, dt=0.01, t0=0., fmin=1., fmax=10., nf=100, w0=6, left=0.1, bottom=0.1, h_1=0.2, h_2=0.6, w_1=0.2, w_2=0.6, w_cb=0.01, d_cb=0.0, show=True, plot_args=['k', 'k'], clim=0.0, cmap=obspy_sequential, mode='absolute', fft_zero_pad_fac=0): import matplotlib.pyplot as plt from matplotlib.ticker import NullFormatter npts = st.shape[-1] tmax = (npts - 1) * dt t = np.linspace(0., tmax, npts) + t0 if fft_zero_pad_fac == 0: nfft = npts else: nfft = util.next_pow_2(npts) * fft_zero_pad_fac f_lin = np.linspace(0, 0.5 / dt, nfft // 2 + 1) if len(st.shape) == 1: _w = np.zeros((1, nf, npts), dtype=complex) _w[0] = cwt(st, dt, w0, fmin, fmax, nf) ntr = 1 spec = np.zeros((1, nfft // 2 + 1), dtype=complex) spec[0] = np.fft.rfft(st, n=nfft) * dt st = st.reshape((1, npts)) else: _w = np.zeros((st.shape[0], nf, npts), dtype=complex) spec = np.zeros((st.shape[0], nfft // 2 + 1), dtype=complex) for i in np.arange(st.shape[0]): _w[i] = cwt(st[i], dt, w0, fmin, fmax, nf) spec[i] = np.fft.rfft(st[i], n=nfft) * dt ntr = st.shape[0] if mode == 'absolute': _tfr = np.abs(_w) spec = np.abs(spec) elif mode == 'power': _tfr = np.abs(_w)**2 spec = np.abs(spec)**2 else: raise ValueError('mode "' + mode + '" not defined!') figs = [] for itr in np.arange(ntr): fig = plt.figure() # plot signals ax_sig = fig.add_axes([left + w_1, bottom, w_2, h_1]) ax_sig.plot(t, st[itr], plot_args[0]) # plot TFR ax_tfr = fig.add_axes([left + w_1, bottom + h_1, w_2, h_2]) x, y = np.meshgrid( t, np.logspace(np.log10(fmin), np.log10(fmax), _tfr[itr].shape[0])) img_tfr = _pcolormesh_same_dim(ax_tfr, x, y, _tfr[itr], cmap=cmap) img_tfr.set_rasterized(True) ax_tfr.set_yscale("log") ax_tfr.set_ylim(fmin, fmax) ax_tfr.set_xlim(t[0], t[-1]) # plot spectrum ax_spec = fig.add_axes([left, bottom + h_1, w_1, h_2]) ax_spec.semilogy(spec[itr], f_lin, plot_args[1]) # add colorbars ax_cb_tfr = fig.add_axes( [left + w_1 + w_2 + d_cb + w_cb, bottom + h_1, w_cb, h_2]) fig.colorbar(img_tfr, cax=ax_cb_tfr) # set limits ax_sig.set_ylim(st.min() * 1.1, st.max() * 1.1) ax_sig.set_xlim(t[0], t[-1]) xlim = spec.max() * 1.1 ax_spec.set_xlim(xlim, 0.) ax_spec.set_ylim(fmin, fmax) if clim == 0.: clim = _tfr.max() img_tfr.set_clim(0., clim) ax_sig.set_xlabel( 'Tiempo [seg] después de 2020-03-04T17:20:10.000000Z') ax_spec.set_ylabel('Frecuencia [Hz]') # remove axis labels ax_tfr.xaxis.set_major_formatter(NullFormatter()) ax_tfr.yaxis.set_major_formatter(NullFormatter()) figs.append(fig) if show: plt.show() else: if ntr == 1: return figs[0] else: return figs
def simulate_seismometer( data, samp_rate, paz_remove=None, paz_simulate=None, remove_sensitivity=True, simulate_sensitivity=True, water_level=600.0, zero_mean=True, taper=True, taper_fraction=0.05, pre_filt=None, seedresp=None, nfft_pow2=False, pitsasim=True, sacsim=False, shsim=False): """ Simulate/Correct seismometer. :type data: NumPy :class:`~numpy.ndarray` :param data: Seismogram, detrend before hand (e.g. zero mean) :type samp_rate: float :param samp_rate: Sample Rate of Seismogram :type paz_remove: dict, None :param paz_remove: Dictionary containing keys 'poles', 'zeros', 'gain' (A0 normalization factor). poles and zeros must be a list of complex floating point numbers, gain must be of type float. Poles and Zeros are assumed to correct to m/s, SEED convention. Use None for no inverse filtering. :type paz_simulate: dict, None :param paz_simulate: Dictionary containing keys 'poles', 'zeros', 'gain'. Poles and zeros must be a list of complex floating point numbers, gain must be of type float. Or None for no simulation. :type remove_sensitivity: bool :param remove_sensitivity: Determines if data is divided by `paz_remove['sensitivity']` to correct for overall sensitivity of recording instrument (seismometer/digitizer) during instrument correction. :type simulate_sensitivity: bool :param simulate_sensitivity: Determines if data is multiplied with `paz_simulate['sensitivity']` to simulate overall sensitivity of new instrument (seismometer/digitizer) during instrument simulation. :type water_level: float :param water_level: Water_Level for spectrum to simulate :type zero_mean: bool :param zero_mean: If true the mean of the data is subtracted :type taper: bool :param taper: If true a cosine taper is applied. :type taper_fraction: float :param taper_fraction: Taper fraction of cosine taper to use :type pre_filt: list or tuple of floats :param pre_filt: Apply a bandpass filter to the data trace before deconvolution. The list or tuple defines the four corner frequencies (f1,f2,f3,f4) of a cosine taper which is one between f2 and f3 and tapers to zero for f1 < f < f2 and f3 < f < f4. :type seedresp: dict, None :param seedresp: Dictionary contains keys 'filename', 'date', 'units'. 'filename' is the path to a RESP-file generated from a dataless SEED volume (or a file like object with RESP information); 'date' is a :class:`~obspy.core.utcdatetime.UTCDateTime` object for the date that the response function should be extracted for (can be omitted when calling simulate() on Trace/Stream. the Trace's starttime will then be used); 'units' defines the units of the response function. Can be either 'DIS', 'VEL' or 'ACC'. :type nfft_pow2: bool :param nfft_pow2: Number of frequency points to use for FFT. If True, the exact power of two is taken (default in PITSA). If False the data are not zero-padded to the next power of two which makes a slower FFT but is then much faster for e.g. evalresp which scales with the FFT points. :type pitsasim: bool :param pitsasim: Choose parameters to match instrument correction as done by PITSA. :type sacsim: bool :param sacsim: Choose parameters to match instrument correction as done by SAC. :type shsim: bool :param shsim: Choose parameters to match instrument correction as done by Seismic Handler. :return: The corrected data are returned as :class:`numpy.ndarray` float64 array. float64 is chosen to avoid numerical instabilities. This function works in the frequency domain, where nfft is the next power of len(data) to avoid wrap around effects during convolution. The inverse of the frequency response of the seismometer (``paz_remove``) is convolved with the spectrum of the data and with the frequency response of the seismometer to simulate (``paz_simulate``). A 5% cosine taper is taken before simulation. The data must be detrended (e.g.) zero mean beforehand. If paz_simulate=None only the instrument correction is done. In the latter case, a broadband filter can be applied to the data trace using pre_filt. This restricts the signal to the valid frequency band and thereby avoids artifacts due to amplification of frequencies outside of the instrument's passband (for a detailed discussion see *Of Poles and Zeros*, F. Scherbaum, Kluwer Academic Publishers). .. versionchanged:: 0.5.1 The default for `remove_sensitivity` and `simulate_sensitivity` has been changed to ``True``. Old deprecated keyword arguments `paz`, `inst_sim`, `no_inverse_filtering` have been removed. """ # Checking the types if not paz_remove and not paz_simulate and not seedresp: msg = "Neither inverse nor forward instrument simulation specified." raise TypeError(msg) for d in [paz_remove, paz_simulate]: if d is None: continue for key in ['poles', 'zeros', 'gain']: if key not in d: raise KeyError("Missing key: %s" % key) # Translated from PITSA: spr_resg.c delta = 1.0 / samp_rate # ndat = len(data) data = data.astype(np.float64) if zero_mean: data -= data.mean() if taper: if sacsim: data *= cosine_taper(ndat, taper_fraction, sactaper=sacsim, halfcosine=False) else: data *= cosine_taper(ndat, taper_fraction) # The number of points for the FFT has to be at least 2 * ndat (in # order to prohibit wrap around effects during convolution) cf. # Numerical Recipes p. 429 calculate next power of 2. if nfft_pow2: nfft = util.next_pow_2(2 * ndat) # evalresp scales directly with nfft, therefore taking the next power of # two has a greater negative performance impact than the slow down of a # not power of two in the FFT else: nfft = _npts2nfft(ndat) # Transform data in Fourier domain data = np.fft.rfft(data, n=nfft) # Inverse filtering = Instrument correction if paz_remove: freq_response, freqs = paz_to_freq_resp( paz_remove['poles'], paz_remove['zeros'], paz_remove['gain'], delta, nfft, freq=True) if seedresp: freq_response, freqs = evalresp(delta, nfft, seedresp['filename'], seedresp['date'], units=seedresp['units'], freq=True, network=seedresp['network'], station=seedresp['station'], locid=seedresp['location'], channel=seedresp['channel']) if not remove_sensitivity: msg = "remove_sensitivity is set to False, but since seedresp " + \ "is selected the overall sensitivity will be corrected " + \ " for anyway!" warnings.warn(msg) if paz_remove or seedresp: if pre_filt: # make cosine taper fl1, fl2, fl3, fl4 = pre_filt if sacsim: cos_win = cosine_sac_taper(freqs, flimit=(fl1, fl2, fl3, fl4)) else: cos_win = cosine_taper(freqs.size, freqs=freqs, flimit=(fl1, fl2, fl3, fl4)) data *= cos_win invert_spectrum(freq_response, water_level) data *= freq_response del freq_response # Forward filtering = Instrument simulation if paz_simulate: data *= paz_to_freq_resp(paz_simulate['poles'], paz_simulate['zeros'], paz_simulate['gain'], delta, nfft) data[-1] = abs(data[-1]) + 0.0j # transform data back into the time domain data = np.fft.irfft(data)[0:ndat] if pitsasim: # linear detrend data = simple_detrend(data) if shsim: # detrend using least squares data = scipy.signal.detrend(data, type="linear") # correct for involved overall sensitivities if paz_remove and remove_sensitivity and not seedresp: data /= paz_remove['sensitivity'] if paz_simulate and simulate_sensitivity: data *= paz_simulate['sensitivity'] return data
def plotFK(st, startTime, endTime, frqlow, frqhigh, sll_x=-3.6, slm_x=3.6, sll_y=-3.6, slm_y=3.6, sl_s=0.18, beam='bartlett', prewhiten=0, coordsys='lonlat', verbose=False, plot=True, normalize=True, cmap='inferno_r', sl_circle=True, interpolation=None, vmin=None, vmax=None, plot_normalize=False, sl_corr=[0., 0.]): ''' Modified from Stephen Arrowsmith's ROSES 2020 class Computes and displays an FK plot for an ObsPy Stream object, st, given a start time and end time (as UTCDateTime objects) and a frequency band defined by frqlow and frqhigh. The slowness grid is defined as optional parameters (in s/km). This function implements code directly from ObsPy, which has been optimized, for simply plotting the FK spectrum It includes the option to normalize the data in the time window before running FK It also includes the option to apply a slowness correction, defined by sl_corr ''' stream = st.copy() stream = stream.trim(startTime, endTime) nstat = len(stream) fk_methods = dict(bartlett=0, capon=1) if nstat > 0: if normalize: for ms in stream: ms.data = ms.data / np.max(np.abs(ms.data)) grdpts_x = int(((slm_x - sll_x) / sl_s + 0.5) + 1) grdpts_y = int(((slm_y - sll_y) / sl_s + 0.5) + 1) geometry = get_geometry(stream, coordsys=coordsys, verbose=verbose) time_shift_table = get_timeshift(geometry, sll_x, sll_y, sl_s, grdpts_x, grdpts_y) fs = stream[0].stats.sampling_rate nsamp = stream[0].stats.npts # generate plan for rfftr nfft = next_pow_2(nsamp) deltaf = fs / float(nfft) nlow = int(frqlow / float(deltaf) + 0.5) nhigh = int(frqhigh / float(deltaf) + 0.5) nlow = max(1, nlow) # avoid using the offset nhigh = min(nfft // 2 - 1, nhigh) # avoid using nyquist nf = nhigh - nlow + 1 # include upper and lower frequency # to speed up the routine a bit we estimate all steering vectors in advance steer = np.empty((nf, grdpts_x, grdpts_y, nstat), dtype=np.complex128) clibsignal.calcSteer(nstat, grdpts_x, grdpts_y, nf, nlow, deltaf, time_shift_table, steer) _r = np.empty((nf, nstat, nstat), dtype=np.complex128) ft = np.empty((nstat, nf), dtype=np.complex128) # 0.22 matches 0.2 of historical C bbfk.c tap = cosine_taper(nsamp, p=0.22) relpow_map = np.empty((grdpts_x, grdpts_y), dtype=np.float64) abspow_map = np.empty((grdpts_x, grdpts_y), dtype=np.float64) for i, tr in enumerate(stream): dat = tr.data dat = (dat - dat.mean()) * tap ft[i, :] = np.fft.rfft(dat, nfft)[nlow:nlow + nf] ft = np.ascontiguousarray(ft, np.complex128) relpow_map.fill(0.) abspow_map.fill(0.) # computing the covariances of the signal at different receivers dpow = 0. for i in range(nstat): for j in range(i, nstat): _r[:, i, j] = ft[i, :] * ft[j, :].conj() if i != j: _r[:, j, i] = _r[:, i, j].conjugate() else: dpow += np.abs(_r[:, i, j].sum()) dpow *= nstat clibsignal.generalizedBeamformer(relpow_map, abspow_map, steer, _r, nstat, prewhiten, grdpts_x, grdpts_y, nf, dpow, fk_methods[beam]) fisher_map = (nstat - 1) * relpow_map / (1 - relpow_map) (ix, iy) = np.unravel_index(relpow_map.argmax(), relpow_map.shape) # here we compute baz, slow slow_x = sll_x + ix * sl_s slow_y = sll_y + iy * sl_s # --------- slow_x = slow_x - sl_corr[0] slow_y = slow_y - sl_corr[1] #print(slow_x, slow_y) # --------- slow = np.sqrt(slow_x**2 + slow_y**2) if slow < 1e-8: slow = 1e-8 azimut = 180 * math.atan2(slow_x, slow_y) / math.pi baz = azimut % -360 + 180 if plot: n_frames = 3 (fig, ax) = plt.subplots(1, n_frames, sharey=True, figsize=(8, 3.5), constrained_layout=True) extent = extent = [sll_x, slm_x + sl_s, sll_y, slm_y + sl_s] # FK power i = 0 H = np.flipud(np.fliplr(abspow_map.T)) if plot_normalize: H = H / H.max() im = ax[i].imshow(H, extent=extent, origin='lower', aspect='auto', cmap=cmap, interpolation=interpolation) plt.colorbar(im, ax=ax[i], orientation="horizontal", label='FK Power') # Semblance i += 1 H = np.flipud(np.fliplr(relpow_map.T)) if plot_normalize: H = H / H.max() im = ax[i].imshow(H, extent=extent, origin='lower', aspect='auto', cmap=cmap, interpolation=interpolation) plt.colorbar(im, ax=ax[i], orientation="horizontal", label='Semblance') # Fisher ratio i += 1 H = np.flipud(np.fliplr(fisher_map.T)) if plot_normalize: H = H / H.max() im = ax[i].imshow(H, extent=extent, origin='lower', aspect='auto', cmap=cmap, interpolation=interpolation) plt.colorbar(im, ax=ax[i], orientation="horizontal", label='Fisher ratio') for i in range(0, n_frames): if sl_circle: angles = np.deg2rad(np.arange(0., 360, 1.)) slowness = dict(seismic_P=6.0, Rayleigh=3.0, infrasound=0.34) for (key, radius) in slowness.items(): x_circle = np.sin(angles) / radius y_circle = np.cos(angles) / radius ax[i].plot(x_circle, y_circle, linestyle='solid', label=key, alpha=0.6) ax[i].plot(0, 0, 'k+') ax[i].plot(-slow_x, -slow_y, 'w+') ax[i].set_xlabel('x-slowness [s/km]') ax[0].set_ylabel('y-slowness [s/km]') baz_max = round(baz % 360., 2) appvel_max = round(1 / slow, 2) title_str = (f'Peak semblance at {baz_max:.2f} deg. ' f'and {appvel_max:.2f} km/s ' f'between [ {frqlow:.2f} - {frqhigh:.2f} ] Hz') fig.suptitle(title_str) return fig, ax # # only flipping left-right, when using imshow to plot the matrix is takes # # points top to bottom points are now starting at top-left in row major # return np.fliplr(relpow_map.T), baz % 360, 1. / slow else: print(f'No data present for timerange {startTime} - {endTime}') return