├── ABAZ_AWAZ.SAC ├── AWAZ_EPAZ.SAC ├── AWAZ_HIZ.SAC ├── GRZ_TOZ.SAC ├── README.md ├── ftan.py └── run.py /ABAZ_AWAZ.SAC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxensing/ftanpy/c5df4f66c86056afb229fc5ec3105ac3f6db9836/ABAZ_AWAZ.SAC -------------------------------------------------------------------------------- /AWAZ_EPAZ.SAC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxensing/ftanpy/c5df4f66c86056afb229fc5ec3105ac3f6db9836/AWAZ_EPAZ.SAC -------------------------------------------------------------------------------- /AWAZ_HIZ.SAC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxensing/ftanpy/c5df4f66c86056afb229fc5ec3105ac3f6db9836/AWAZ_HIZ.SAC -------------------------------------------------------------------------------- /GRZ_TOZ.SAC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxensing/ftanpy/c5df4f66c86056afb229fc5ec3105ac3f6db9836/GRZ_TOZ.SAC -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ftanpy 2 | Frequency-Time ANalysis (FTAN) in python 3 | 4 | Here we compute compute group speeds using FTAN as carried out in my 2020 thesis, Multi-Component Ambient Noise Tomography of the Auckland Volcanic Field. 5 | To test it out, run the run.py script. 6 | 7 | Requirements 8 | python 3, obspy, matplotlib.pyplot, csv, and numpy 9 | -------------------------------------------------------------------------------- /ftan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Wed Mar 28 09:23:42 2018 5 | 6 | @author: Josiah 7 | """ 8 | from obspy import read, read_inventory 9 | from obspy.geodetics.base import gps2dist_azimuth 10 | import matplotlib.pyplot as plt 11 | import csv 12 | from numpy import fft 13 | import numpy as np 14 | 15 | def fold_trace(tr): 16 | """fold_trace() takes a trace and computes the symmetric component by 17 | summing the time-reversed acausal component with the causal coponent""" 18 | # Pick some constants we'll need from the trace: 19 | npts2 = tr.stats.npts 20 | npts = int((npts2-1)/2) 21 | 22 | # Fold the CCFs: 23 | tr_folded= tr.copy() 24 | causal = tr.data[npts:-1] 25 | acausal = tr.data[npts:0:-1] 26 | tr_folded.data = (causal+acausal)/2 27 | 28 | return tr_folded 29 | 30 | def interstation_distance(filename): 31 | """compute the interstation distance from file name. 32 | filenames should as stationA.stationB.components.SAC 33 | e.g. ABAZ.KBAZ.ZT.SAC""" 34 | 35 | 36 | #read inventory to get station coordinates and calculate interstation distance 37 | inv = read_inventory("inv.XML") 38 | 39 | x=filename.split("_") 40 | stationA=inv.select(station=x[0]) 41 | stationB=inv.select(station=x[1]) 42 | distance, az, baz = gps2dist_azimuth(stationA[0][0].latitude,stationA[0][0].longitude,stationB[0][0].latitude,stationB[0][0].longitude) 43 | 44 | return distance 45 | 46 | def make_gaussian_filters(array,samp_rate, dist, freqmin=0.01,freqmax=10,filt_no=50,alpha=10): 47 | """Make a set of Gaussian filters. 48 | :array : Real component of FFT of seismic signal. Filter lengths need to be the same length as this trace 49 | :freqmin : lowest center frequency 50 | :freqmax : maximum center frequency 51 | :filt_no : number of filters 52 | :alpha : adjustable parameter that sets the resolution in the frequency and time domains. Default = 1 53 | """ 54 | frequencies = np.arange(len(array))*samp_rate/len(array) 55 | filters=[] 56 | central_frequencies = np.logspace(np.log10(freqmin),np.log10(freqmax),filt_no) 57 | #omega_0 is the center frequency of each Gaussian filter 58 | for omega_0 in central_frequencies: 59 | gaussian=[] 60 | if 10 < dist < 100: 61 | alpha=10 62 | elif 100 < dist < 250: 63 | alpha= 30 64 | elif dist > 250: 65 | alpha = 100 66 | elif dist < 10: 67 | alpha = 0.2 68 | for omega in frequencies: 69 | gaussian.append(np.e**(-alpha*((omega-omega_0)/omega_0)**2)) 70 | filters.append([[omega_0],gaussian]) 71 | 72 | return filters, frequencies 73 | 74 | def ftan(filename,freqmin=0.1,freqmax=10,vmin=1.0,vmax=5.0,fold=False,alpha=10): 75 | """returns the filename, and computes and saves FTAN amplitudes and group velocities. 76 | :filename : name of sac file e.g. ABAZ_ETAZ_ZZ.SAC 77 | :freqmin : minimum frequency for filter 78 | :freqmax : maximum frequency for filter 79 | :alpha : adjustable parameter that sets the resolution in the frequency and time domains. Default = 10 80 | """ 81 | st=read(filename) 82 | tr=st[0] 83 | if fold==True: 84 | tr = fold_trace(tr) 85 | samp_rate = tr.stats.sampling_rate #sampling rate 86 | samp_int = tr.stats.delta #sampling interval 87 | t = np.arange(0,len(tr.data)*samp_int,samp_int) # time vector 88 | dist=tr.stats.sac["dist"]/1000 89 | 90 | if freqmax > samp_rate/2: 91 | print("Maximum frequency exceeded the Nyquist frequency") 92 | print("Freqmax = {}, Nyquist = {}".format(str(freqmax),str(samp_rate/2))) 93 | print("Maximum frequency reset to {}".format(str(samp_rate/2))) 94 | freqmax = samp_rate/2 95 | 96 | tr_ft = fft.fft(tr.data) # Fourier transform tr into the frequency domain 97 | 98 | #take the analytic component by... 99 | tr_af = tr_ft.copy() 100 | tr_af[:len(tr_af)//2]*=2.0 #multiplying all the positive frequencies by two 101 | tr_af[len(tr_af)//2:]*=0.0 #multiplying all the negative frequencies by zero 102 | 103 | gaussian_filters, frequencies = make_gaussian_filters(tr_ft.real,freqmin=freqmin, 104 | freqmax=freqmax,samp_rate=samp_rate,dist=dist, alpha=alpha) 105 | 106 | filename_amplitudes = ".".join([filename,"amplitudes.csv"]) 107 | headers=["Speed (km/s)","Centre Period (s)","Instaneous Period (s)", 108 | "Amplitude"] 109 | 110 | with open(filename_amplitudes,"w",newline='') as csvfile: 111 | writer=csv.writer(csvfile) 112 | writer.writerow(headers) 113 | group_speeds=[] 114 | gs_c_period=[] 115 | gs_inst_period=[] 116 | envelope_max=[] 117 | phase_at_group_time = [] 118 | neg_errors = [] 119 | pos_errors = [] 120 | 121 | for g_filter in gaussian_filters: 122 | tr_aff = tr_af*np.array(g_filter[1]) #applying the filters to the analytic signal in frequency domain 123 | tr_at = fft.ifft(tr_aff) # inverse Fourier transform of the filtered analytic signal 124 | envelope=np.log(np.absolute(tr_at)**2)# taking the logarithm of the square of the filtered analytic signal in the time domain 125 | # phase_function = np.unwrap(np.angle(tr_at)) # compute the unwrapped phase function 126 | phase_function = np.angle(tr_at) # compute the phase function 127 | phase_at_group_time.append(phase_function[np.argmax(envelope)]) # save phase at group time 128 | omega_inst = np.diff(phase_function)/(samp_int*2*np.pi) # compute instaneous frequencies for phase function 129 | omega_inst = omega_inst[np.argmax(envelope)-1] 130 | 131 | # omega_inst = omega_inst[np.argmin(np.abs(omega_inst-g_filter[0]))] # compute instaneous frequencies 132 | center_period=np.around(float(1/g_filter[0][0]),decimals=2) 133 | instantaneous_period = np.around(float(1/omega_inst),decimals=2) 134 | # phi_s = 0 #source phase is zero for ambient noise cross-correlations 135 | 136 | 137 | for i, amplitude in enumerate(envelope.real): 138 | if t[i] != 0 and vmin < dist/t[i] < vmax: 139 | speed=float(dist/t[i]) 140 | writer.writerow([speed,center_period,instantaneous_period,amplitude]) 141 | else: 142 | pass 143 | 144 | #compute group speeds and related data 145 | if t[np.argmax(envelope)] != 0 and vmin < dist/t[np.argmax(envelope)] < vmax: 146 | envelope_max.append(max(envelope)) 147 | group_speeds.append(dist/t[np.argmax(envelope)]) 148 | gs_c_period.append(center_period) 149 | gs_inst_period.append(instantaneous_period) 150 | 151 | # Error Analysis 152 | #upper error 153 | e_up=envelope[np.argmax(envelope):] 154 | t_up=t[np.argmax(envelope):] 155 | amp_up=[] 156 | for i, amp in enumerate(e_up): 157 | if amp >= max(envelope)-0.5: 158 | amp_up.append([t_up[i],amp]) 159 | else: 160 | break 161 | if len(amp_up)>1: 162 | pos_error=dist/amp_up[-1][0]-dist/amp_up[0][0] 163 | else: 164 | pos_error=10 165 | if pos_error==float("Inf"): 166 | neg_error=10 167 | pos_errors.append(abs(pos_error)) 168 | 169 | #lower error 170 | e_dwn=envelope[:np.argmax(envelope)+1] 171 | e_dwn=e_dwn[::-1] 172 | t_dwn=t[:np.argmax(envelope)+1] 173 | t_dwn=t_dwn[::-1] 174 | amp_dwn=[] 175 | for i, amp in enumerate(e_dwn): 176 | if amp >= max(envelope)-0.5: 177 | amp_dwn.append([t_dwn[i],amp]) 178 | else: 179 | break 180 | 181 | if amp_dwn[-1][0] !=0 and amp_dwn[0][0] !=0: 182 | neg_error = dist/amp_dwn[-1][0]-dist/amp_dwn[0][0] 183 | else: 184 | neg_error = 10 185 | # print(center_period) 186 | neg_errors.append(abs(neg_error)) 187 | 188 | filename_group_speeds = ".".join([filename,"group_speeds","csv"]) 189 | headers=["Group Speed (km/s)","Centre Period (s)","Instaneous Period (s)","Negative Error","Postive Error","time (s)","distance (km)"] 190 | # headers=["Group Speed (km/s)","Centre Period (s)"] 191 | group_speeds,gs_c_period,gs_inst_period,phase_at_group_time = trim(group_speeds,gs_c_period,gs_inst_period,envelope_max,phase_at_group_time) 192 | with open(filename_group_speeds,"w",newline='') as csvfile: 193 | writer=csv.writer(csvfile) 194 | writer.writerow(headers) 195 | for i, group_speed in enumerate(group_speeds): 196 | writer.writerow([group_speed,gs_c_period[i],gs_inst_period[i],neg_errors[i],pos_errors[i], dist/group_speed, dist]) 197 | # writer.writerow([group_speed,gs_c_period[i]]) 198 | 199 | def trim(group_speeds,gs_c_period,gs_inst_period,envelope_max,phase_at_group_time,jthresh=0.2,ampthresh=-15): 200 | """Returns group speed as a function of period trimmed to avoid large jumps caused by noise and low amplitude envelopes. 201 | :group_speeds : 202 | :gs_c_period : 203 | :gs_inst_period : 204 | :envelope_max : List of envelope maxima (track of the amplitude ridge in FTAN plot) 205 | :jthresh : Jump threshold. Discards groups speeds to avoid jumps larger than jthresh. Default = 0.2 206 | :ampthresh : Amplitude threshold. Discards groups speeds if the envelope maximum is below he threshold . Default = -15 207 | """ 208 | del_array=[] 209 | for i in range(len(envelope_max)): 210 | if envelope_max[i] < ampthresh: 211 | del_array.append(i) 212 | for i in del_array[::-1]: 213 | del group_speeds[i] 214 | del gs_c_period[i] 215 | del gs_inst_period[i] 216 | del envelope_max[i] 217 | del phase_at_group_time[i] 218 | 219 | if len(np.where(abs(np.diff(group_speeds))>jthresh)[0]) > 0: 220 | gs1=group_speeds[envelope_max.index(max(envelope_max)):] 221 | cp1=gs_c_period[envelope_max.index(max(envelope_max)):] 222 | ip1=gs_inst_period[envelope_max.index(max(envelope_max)):] 223 | ph1=phase_at_group_time[envelope_max.index(max(envelope_max)):] 224 | 225 | gs2=group_speeds[:envelope_max.index(max(envelope_max))] 226 | cp2=gs_c_period[:envelope_max.index(max(envelope_max))] 227 | ip2=gs_inst_period[:envelope_max.index(max(envelope_max))] 228 | ph2=phase_at_group_time[:envelope_max.index(max(envelope_max))] 229 | gs1_diff=abs(np.diff(gs1)) 230 | gs2_diff=abs(np.diff(gs2)) 231 | 232 | if len(np.where(gs1_diff>jthresh)[0]) == 0: 233 | step_int1 = len(gs1_diff) 234 | elif len(np.where(gs1_diff>jthresh)[0]) > 0: 235 | step_int1 = min(np.where(gs1_diff>jthresh)[0])+1 236 | 237 | if len(np.where(gs2_diff>jthresh)[0]) == 0: 238 | step_int2 = 0 239 | elif len(np.where(gs2_diff>jthresh)[0]) > 0: 240 | step_int2=max(np.where(gs2_diff>jthresh)[0])+1 241 | 242 | group_speeds =np.append(gs1[:step_int1],gs2[step_int2:]) 243 | gs_c_period =np.append(cp1[:step_int1],cp2[step_int2:]) 244 | gs_inst_period =np.append(ip1[:step_int1],ip2[step_int2:]) 245 | phase_at_group_time=np.append(ph1[:step_int1],ph2[step_int2:]) 246 | gs_inst_period,gs_c_period, group_speeds, phase_at_group_time= zip(*sorted(zip(gs_inst_period, gs_c_period, group_speeds, phase_at_group_time))) 247 | 248 | return group_speeds,gs_c_period,gs_inst_period,phase_at_group_time 249 | 250 | def plot(filename,group_speeds=False,phase_speeds=False,fplot=False,three_d=False): 251 | import numpy as np 252 | import matplotlib.pyplot as plt 253 | array1=np.loadtxt(filename+".amplitudes.csv",delimiter=',',skiprows=1) 254 | speeds=array1[:,0] 255 | # if inst_period==True: 256 | # periods=array1[:,2] 257 | # else: 258 | # periods=array1[:,1] 259 | centre_periods=array1[:,1] 260 | instantaneous_periods=array1[:,2] 261 | if fplot==True: 262 | # periods=np.reciprocal(periods) 263 | instantaneous_periods = np.reciprocal(instantaneous_periods) 264 | centre_periods= np.reciprocal(centre_periods) 265 | amplitudes=array1[:,3] 266 | if fplot==False: 267 | x = np.unique(centre_periods)[::-1] 268 | else: 269 | x = np.unique(centre_periods) 270 | 271 | y = np.unique(speeds)[::-1] 272 | X, Y = np.meshgrid(x, y) 273 | Z = amplitudes.reshape(len(x),len(y)).T 274 | 275 | plt.figure(figsize=(5,5)) 276 | plt.title(filename) 277 | plt.contourf(X,Y,Z,50,cmap="jet") 278 | 279 | if group_speeds is True: 280 | try: 281 | array2=np.loadtxt(filename+".group_speeds.csv",delimiter=',',skiprows=1) 282 | gspeeds=array2[:,0] 283 | centre_periods=array2[:,1] 284 | instantaneous_periods=array2[:,2] 285 | 286 | if fplot==False: 287 | plt.scatter(centre_periods,gspeeds,label="group speed",color="w",marker="+") 288 | plt.scatter(instantaneous_periods, gspeeds, label="instantaneous group speed", color="k", marker="x") 289 | else: 290 | plt.scatter(np.reciprocal(centre_periods),gspeeds,label="group speed",color="w",marker="+") 291 | plt.legend(loc=1) 292 | except: 293 | print("Input file may be empty") 294 | 295 | if phase_speeds is True: 296 | try: 297 | array2=np.loadtxt(filename+".phase_speeds.csv",delimiter=',',skiprows=1) 298 | 299 | centre_periods=array2[:,0] 300 | instantaneous_periods=array2[:,1] 301 | # plt.scatter(instantaneous_periods,gspeeds,label="group speed",color="k",marker="x") 302 | for N_column in range(2,10): 303 | if fplot==False: 304 | plt.scatter(centre_periods,array2[:,N_column],label="phase speed",color="k",marker=".") 305 | else: 306 | plt.scatter(np.reciprocal(centre_periods),array2[:,N_column],label="phase speed",color="k",marker=".") 307 | plt.legend(loc=1) 308 | except: 309 | print("Input file may be empty") 310 | 311 | plt.ylim(1.0,5.0) 312 | plt.xlim(1,10) 313 | plt.xlabel("Period (s)") 314 | if fplot == True: 315 | plt.xlabel("Frequency (Hz)") 316 | plt.xlim(0.1,1) 317 | plt.ylabel("Speed (kms$^{-1}$)") 318 | plt.grid() 319 | plt.savefig(filename+"_ftanplot.pdf") 320 | plt.show() 321 | 322 | if phase_speeds is True: 323 | plt.figure(figsize=(5,5)) 324 | plt.title(filename) 325 | try: 326 | array2=np.loadtxt(filename+".phase_speeds.csv",delimiter=',',skiprows=1) 327 | 328 | centre_periods=array2[:,0] 329 | instantaneous_periods=array2[:,1] 330 | # plt.scatter(instantaneous_periods,gspeeds,label="group speed",color="k",marker="x") 331 | for N_column in range(2,10): 332 | if fplot==False: 333 | plt.scatter(centre_periods,array2[:,N_column],color="k",marker=".") 334 | else: 335 | plt.scatter(np.reciprocal(centre_periods),array2[:,N_column],color="k",marker=".") 336 | except: 337 | print("Input file may be empty") 338 | plt.ylim(1.0,5.0) 339 | plt.xlim(1,10) 340 | # plt.xscale('log') 341 | plt.xlabel("Period (s)") 342 | if fplot == True: 343 | plt.xlabel("Frequency (Hz)") 344 | # plt.xlim(0.1,1) 345 | print('done') 346 | plt.ylabel("Speed (kms$^{-1}$)") 347 | plt.grid() 348 | plt.savefig(filename+"_phase_speed.pdf") 349 | plt.show() 350 | 351 | if three_d==True: 352 | import matplotlib as mpl 353 | from mpl_toolkits.mplot3d import Axes3D 354 | import numpy as np 355 | import matplotlib.pyplot as plt 356 | 357 | mpl.rcParams['legend.fontsize'] = 10 358 | 359 | fig = plt.figure() 360 | ax = fig.gca(projection='3d') 361 | ax.set_xlabel("Period (s)") 362 | ax.set_ylabel("Speed (kms$^{-1}$)") 363 | ax.set_zlabel("Amplitude") 364 | X = centre_periods.reshape(len(x), len(y)).T 365 | Y = speeds.reshape(len(x), len(y)).T 366 | Z = amplitudes.reshape(len(x), len(y)).T 367 | # for column in range(0,Z.shape[1]): 368 | # ax.plot(X[:,column],Y[:,column], Z[:,column]) 369 | # ax.plot(centre_periods,speeds, amplitudes, label='parametric curve') 370 | ax.plot_wireframe(X, Y, Z, rstride=30, cstride=3) 371 | # rotate the axes and update 372 | # ax.view_init(30, 0) 373 | # for angle in range(0, 360): 374 | # ax.view_init(30, angle) 375 | # plt.draw() 376 | # plt.pause(.001) 377 | 378 | # if group_speeds is True: 379 | # try: 380 | # array2 = np.loadtxt(filename + ".group_speeds.csv", delimiter=',', skiprows=1) 381 | # gspeeds = array2[:, 0] 382 | # centre_periods = array2[:, 1] 383 | # instantaneous_periods = array2[:, 2] 384 | # amplitudes=array2[:, 3] 385 | # # plt.scatter(instantaneous_periods,gspeeds,label="instantaneous group speed",color="k",marker="x") 386 | # ax.scatter(centre_periods, gspeeds, amplitudes, color="r") 387 | # except: 388 | # print("Input file may be empty") 389 | plt.show() -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from obspy import read 2 | import os 3 | import ftan as ft 4 | 5 | streamfiles=os.listdir() 6 | GFs=[] 7 | 8 | for streamfile in streamfiles: 9 | x = streamfile.split(".") 10 | if x[-1]=="SAC": 11 | st=read(streamfile) 12 | print(x[0]) 13 | filename=ft.ftan(streamfile,freqmin=1/15,freqmax=1) 14 | ft.plot(streamfile, group_speeds=True) --------------------------------------------------------------------------------