def get_shifts(st, s, baz): ''' Calculates the shifts (as an integer number of samples in the time series) for every station in a stream of time series seismograms for a slowness vector of given magnitude and backazimuth. The shift is that which needs to be applied in order to align an arrival (arriving with slowness s and backazimuth baz) with the same arrival at the array reference point (the location of the station that makes up the first trace in the stream). Parameters ---------- st : ObsPy Stream object Stream of SAC format seismograms for the seismic array, length K = no. of stations in array s : float Magnitude of slowness vector, in s / km baz : float Backazimuth of slowness vector, (i.e. angle from North back to epicentre of event) Returns ------- shifts : list List of integer delays at each station in the array, also length K ''' theta = [ ] # Angular position of each station, measured clockwise from North r = [] # Distance of each station # First station is reference point, so has zero position vector theta.append(0.0) r.append(0.0) geometry = get_station_coordinates(st) / 1000. # in km # For each station, get distance from array reference point (first station), and the angular displacement clockwise from north for station in geometry[1:]: r_x = station[0] # x-component of position vector r_y = station[1] # y-component of position vector # theta is angle c/w from North to position vector of station; need to compute diffently for each quadrant if r_x >= 0 and r_y >= 0: theta.append(np.degrees(np.arctan(r_x / r_y))) elif r_x > 0 and r_y < 0: theta.append(180 + np.degrees(np.arctan(r_x / r_y))) elif r_x <= 0 and r_y <= 0: theta.append(180 + np.degrees(np.arctan(r_x / r_y))) else: theta.append(360 + np.degrees(np.arctan(r_x / r_y))) r.append(np.sqrt(r_x**2 + r_y**2)) # Find angle between station position vector and slowness vector in order to compute dot product # Angle between slowness and position vectors, measured clockwise phi = [180 - baz + th for th in theta] sampling_rate = st[0].stats.sampling_rate shifts = [] # Shift is dot product. The minus sign is because a positive time delay needs to be corrected by a negative shift in order to stack for i in range(0, len(st)): shifts.append( -1 * int(round(r[i] * s * np.cos(np.radians(phi[i])) * sampling_rate))) return shifts
def get_shifts(st, s, baz): """ Calculates the shifts (as an integer number of samples in the time series) for every station in a stream of time series seismograms for a slowness vector of given magnitude and backazimuth. The shift is that which needs to be applied in order to align an arrival (arriving with slowness s and backazimuth baz) with the same arrival at the array reference point (the location of the station that makes up the first trace in the stream). Parameters ---------- st : ObsPy Stream object Stream of SAC format seismograms for the seismic array, length K = no. of stations in array s : float Magnitude of slowness vector, in s / km baz : float Backazimuth of slowness vector, (i.e. angle from North back to epicentre of event) Returns ------- shifts : list List of integer delays at each station in the array, also length K """ theta = [] # Angular position of each station, measured clockwise from North r = [] # Distance of each station # First station is reference point, so has zero position vector theta.append(0.0) r.append(0.0) geometry = get_station_coordinates(st) / 1000.0 # in km # For each station, get distance from array reference point (first station), and the angular displacement clockwise from north for station in geometry[1:]: r_x = station[0] # x-component of position vector r_y = station[1] # y-component of position vector # theta is angle c/w from North to position vector of station; need to compute diffently for each quadrant if r_x >= 0 and r_y >= 0: theta.append(np.degrees(np.arctan(r_x / r_y))) elif r_x > 0 and r_y < 0: theta.append(180 + np.degrees(np.arctan(r_x / r_y))) elif r_x <= 0 and r_y <= 0: theta.append(180 + np.degrees(np.arctan(r_x / r_y))) else: theta.append(360 + np.degrees(np.arctan(r_x / r_y))) r.append(np.sqrt(r_x ** 2 + r_y ** 2)) # Find angle between station position vector and slowness vector in order to compute dot product # Angle between slowness and position vectors, measured clockwise phi = [180 - baz + th for th in theta] sampling_rate = st[0].stats.sampling_rate shifts = [] # Shift is dot product. The minus sign is because a positive time delay needs to be corrected by a negative shift in order to stack for i in range(0, len(st)): shifts.append(-1 * int(round(r[i] * s * np.cos(np.radians(phi[i])) * sampling_rate))) return shifts
def fk_analysis(st, smax, fmin, fmax, tmin, tmax, stat='power'): ''' Performs frequency-wavenumber space (FK) analysis on a stream of time series data for a given slowness range, frequency band and time window. For an input stream of length K, the output is a K x K array with values of the chosen statistic calculated on a slowness grid in the x and y spatial dimensions. This statistic can be one of:- * 'power' - the power in frequency-domain stack * 'semblance' - the semblance calculated in the frequency domain * 'F' - the F-statistic calculated in the frequency domain Before the FK analysis is performed, the seismograms are cut to a time window between tmin and tmax, and the data is bandpass-filtered between frequencies fmin and fmax. Parameters ---------- st : ObsPy Stream object Stream of SAC format seismograms for the seismic array, length K = no. of stations in array smax : float Maximum magnitude of slowness, used for constructing the slowness grid in both x and y directions fmin : float Lower end of frequency band to perform FK analysis over fmax : float Upper end of frequency band to perform FK analysis over tmin : float Start of time window, seismograms are cut between tmin and tmax before FK starts tmax : int End of time window, seismograms are cut between tmin and tmax before FK starts stat : string Statistic that is to be calculated over the slowness grid, either 'power', 'semblance', or 'F' Returns ------- fk : NumPy array The chosen statistic calculated at each point in a K x K grid of slowness values in the x and y directions ''' assert (stat == 'power' or stat == 'semblance' or stat == 'F'), "Argument 'stat' must be one of 'power', 'semblance' or 'F'" st = st.copy().trim(starttime=tmin, endtime=tmax) # Retrieve metadata: time step and number of stations to be stacked delta = st[0].stats.delta nbeam = len(st) # Pre-process, and filter to frequency window st.detrend() st.taper(type='cosine', max_percentage=0.05) st = st.copy().filter("bandpass", freqmin=fmin, freqmax=fmax) npts = st[0].stats.npts # Computer Fourier transforms for each trace fft_st = np.zeros((nbeam, (npts / 2) + 1), dtype=complex) # Length of real FFT is only half that of time series data for i, tr in enumerate(st): fft_st[i, :] = np.fft.rfft(tr.data) # Only need positive frequencies, so use rfft freqs = np.fft.fftfreq(npts, delta)[0:(npts / 2) + 1] # Slowness grid slow_x = np.linspace(-smax, smax, nbeam) slow_y = np.linspace(-smax, smax, nbeam) # Array geometry x, y = np.split(get_station_coordinates(st)[:, :2], 2, axis=1) # convert to km x /= 1000. y /= 1000. # Calculate the F-K spectrum fk = np.zeros((nbeam, nbeam)) for ii in range(nbeam): for jj in range(nbeam): dt = slow_x[jj] * x + slow_y[ii] * y beam = np.sum(np.exp(-1j * 2 * np.pi * np.outer(dt, freqs)) * fft_st / nbeam, axis=0) fk[ii, jj] = np.vdot(beam, beam).real if stat == 'semblance' or stat == 'F': tracepower = np.vdot(fft_st, fft_st).real if stat == 'semblance': fk_semb = nbeam * fk / tracepower return fk_semb elif stat == 'F': fk_F = (nbeam - 1)* nbeam * fk / (tracepower - nbeam * fk) return fk_F else: return fk