I have a report with several subplots with the x axis in seconds after the event. I want to add a secondary axis, showing the UTC. I have seen plots that others have posted that does just this.
I have a successful secondary axis working on my PSD plot, so I suspect the problem lies in handling the date/time values and perhaps the format of UTCDateTime (though I have tried converting to datetime, etc). I’d appreciate any help anyone can offer about where I’m going wrong and/or what I’m missing.
Here is my code so far:
from obspy.clients.fdsn import Client
from obspy.core import UTCDateTime, Stream
from obspy.signal import filter
import matplotlib.pyplot as plt
import numpy as np
import datetime
import matplotlib.dates as mdates
from obspy.taup import TauPyModel
from matplotlib.ticker import AutoMinorLocator
import math
rs = Client('RASPISHAKE')
def plot_arrivals(ax):
y1 = -1
axb, axt = ax.get_ylim() # calculate the y limits of the graph
for q in range(0, no_arrs): #plot each arrival in turn
x1 = arrs[q].time # extract the time to plot
if (x1 >= delay):
if x1 < delay+duration:
ax.axvline(x=x1, linewidth=0.5, linestyle='--', color='black') # draw a vertical line
if y1 < 0: # alternate top and bottom for phase tags
y1 = axt*0.8
else:
y1 = axb*0.95
ax.text(x1,y1,arrs[q].name, alpha=0.5) # print the phase name
def time2UTC(a): #convert time (seconds) since event back to UTCDateTime
tUTC = eventTime + a
return tUTC.strftime('%H:%M:%S')
def uTC2time(a): #convert UTCDateTime to seconds since the event
return a - eventTime
def one_over(a): # 1/x to convert frequency to period
#Vectorized 1/a, treating a==0 manually
a = np.array(a).astype(float)
near_zero = np.isclose(a, 0)
a[near_zero] = np.inf
a[~near_zero] = 1 / a[~near_zero]
return a
inverse = one_over #function 1/x is its own inverse
# set the station name and download the response information
stn = 'R21C0' # your station name
inv = rs.get_stations(network='AM', station=stn, level='RESP') # get the instrument response
latS = -33.7691 #station latitude
lonS = 149.8843 #station longitude
eleS = 1114 #station elevation
#enter event data
eventTime = UTCDateTime(2022, 7, 24, 19, 30, 51) # (YYYY, m, d, H, M, S)
latE = -14.9 # quake latitude
lonE = 167.4 # quake longitude
depth = 129 # quake depth, km
mag = 5.0 # quake magnitude
eventID = 'rs2022olyyms' # ID for the event
locE = 'Vanuatu Islands' # location name
#set start time
delay = 120 # delay the start of the plot from the event
duration = 900 # duration of plots
start = eventTime + delay # calculate the plot start time from the event and delay
end = start + duration # calculate the end time from the start and duration
# set the FDSN server location and channel names
ch = 'EHZ' # ENx = accelerometer channels; EHx or SHZ = geophone channels
# get waveform and copy it for independent removal of instrument response
trace0 = rs.get_waveforms('AM', stn, '00', ch, start, end)
trace0.merge(method=0, fill_value='latest') #fill in any gaps in the data to prevent a crash
trace0.detrend(type='demean') #demean the data
trace1 = trace0.copy()
trace2 = trace0.copy()
# calculate great circle angle of separation
# convert angles to radians
latSrad = math.radians(latS)
lonSrad = math.radians(lonS)
latErad = math.radians(latE)
lonErad = math.radians(lonE)
if lonSrad > lonErad:
lon_diff = lonSrad - lonErad
else:
lon_diff = lonErad - lonSrad
great_angle_rad = math.acos(math.sin(latErad)*math.sin(latSrad)+math.cos(latErad)*math.cos(latSrad)*math.cos(lon_diff))
great_angle_deg = math.degrees(great_angle_rad) #great circle angle between quake and station
distance = great_angle_rad*12742/2 #calculate distance between quake and station in km
model = TauPyModel(model='iasp91')
arrs = model.get_travel_times(depth, great_angle_deg)
print(arrs)
no_arrs = len(arrs) # the number of arrivals
# bandpass filter
filt = [0.7, 0.7, 45, 50]
# Create output traces
outdisp = trace0.remove_response(inventory=inv,pre_filt=filt,output='DISP',water_level=60, plot=False) # convert to Disp
outvel = trace1.remove_response(inventory=inv,pre_filt=filt,output='VEL',water_level=60, plot=False) # convert to Vel
outacc = trace2.remove_response(inventory=inv,pre_filt=filt,output='ACC',water_level=60, plot=False) # convert to Acc
# set up plot
fig = plt.figure(figsize=(20,12)) # set to page size in inches
ax1 = fig.add_subplot(5,2,1) # displacement waveform
ax2 = fig.add_subplot(5,2,3) # velocity Waveform
ax3 = fig.add_subplot(5,2,5) # acceleration waveform
ax4 = fig.add_subplot(5,2,7) # velocity spectrogram
ax5 = fig.add_subplot(5,2,9) # velocity PSD
ax7 = fig.add_subplot(5,2,(2,8), polar=True) # TAUp plot
fig.suptitle("AM."+stn+" M"+str(mag)+" "+locE+" "+eventID+eventTime.strftime(' %d/%m/%Y %H:%M:%S.%f UTC')) #Title of the figure
fig.text(0.05, 0.92, "Filter: "+str(filt[1])+" to "+str(filt[2])+"Hz") # Filter details
fig.text(0.2, 0.92, 'Separation = '+str(round(great_angle_deg,3))+u"\N{DEGREE SIGN}"+' or '+str(int(distance))+'km.') #distance between quake and station
fig.text(0.4, 0.92, 'Latitude: '+str(latE)+u"\N{DEGREE SIGN}"+' Longitude: '+str(lonE)+u"\N{DEGREE SIGN}"+' Depth: '+str(depth)+'km.') #quake lat, lon and depth
#plot traces
ax1.plot(trace0[0].times(reftime=eventTime), outdisp[0].data, lw=1, color='b') # displacement waveform
#ax1.set_ylim(-3e-8,3e-8) # set manual y limits for displacement- comment this out for autoscaling
ax2.plot(trace0[0].times(reftime=eventTime), outvel[0].data, lw=1, color='g') # velocity Waveform
#ax2.set_ylim(-1e-7,1e-7) # set manual y limits for velocity - comment this out for autoscaling
ax3.plot(trace0[0].times(reftime=eventTime), outacc[0].data, lw=1, color='r') # acceleration waveform
#ax3.set_ylim(-1e-6,1e-6) # set manual y limits for acceleration - comment this out for auto scaling
ax4.specgram(x=trace1[0], NFFT=32, noverlap=2, Fs=100, cmap='viridis') # velocity spectrogram
ax4.set_ylim(0,filt[3])
ax5.psd(x=trace1[0], NFFT=duration, noverlap=0, Fs=100, color='g', lw=1) # velocity PSD
ax5.set_xscale('log')
#plot secondary axes
secax_x1 = ax1.secondary_xaxis('top', functions=(time2UTC, uTC2time))
secax_x1.set_xlabel('UTC')
secax_x5 = ax5.secondary_xaxis('top', functions=(one_over, inverse))
secax_x5.set_xlabel('Period, s', labelpad=0)
#test conversion functions for secondary time axes
print(time2UTC(120))
print(uTC2time(eventTime+360))
# build array of arrival data
x2 = 0.49 #start near middle of page but maximise list space
dx = 0.0066 # linespacing
fig.text(x2, 0.03, 'Phase',size='small', rotation=90) #print headings
fig.text(x2, 0.09, 'Time',size='small', rotation=90)
fig.text(x2, 0.15, 'UTC',size='small', rotation=90)
fig.text(x2, 0.2, 'Vertical Component', alpha = 0.5, size='small', rotation=90)
for i in range (0, no_arrs): #print data array
x2 += dx
fig.text(x2, 0.03, arrs[i].name, size='small', rotation=90) #print phase name
fig.text(x2, 0.09, str(round(arrs[i].time,3))+'s', size='small', rotation=90) #print arrival time
arrtime = eventTime + arrs[i].time
fig.text(x2, 0.15, arrtime.strftime('%H:%M:%S'), size='small', rotation=90)
if arrs[i].name.endswith('P') or arrs[i].name.endswith('p') or arrs[i].name.endswith('Pdiff') or arrs[i].name.endswith('Pn'): #calculate and print the vertical component of the signal
fig.text(x2, 0.2, str(round(100*math.cos(math.radians(arrs[i].incident_angle)),1))+'%', alpha = 0.5, size='small', rotation=90)
elif arrs[i].name.endswith('S') or arrs[i].name.endswith('s') or arrs[i].name.endswith('Sn'):
fig.text(x2, 0.2, str(round(100*math.sin(math.radians(arrs[i].incident_angle)),1))+'%', alpha = 0.5, size='small', rotation=90)
x2 += 2*dx
fig.text(x2, 0.03, str(no_arrs)+' arrivals total.', size='small', rotation=90) #print number of arrivals
# print phase key
x2 = 0.91
fig.text(x2, 0.03, 'Phase Key', size='small', rotation=90)
pkey = ['P: compression wave', 'p: strictly upward compression wave', 'S: shear wave', 's: strictly upward shear wave', 'K: compression wave in outer core', 'I: compression wave in inner core', 'c: reflection off outer core', 'diff: diffracted wave along core mantle boundary', 'i: reflection off inner core', 'n: wave follows the Moho (crust/mantle boundary)']
for i in range (0, 10):
x2 +=dx
fig.text(x2, 0.03, pkey[i], size='small', rotation=90)
#plot phase arrivals
plot_arrivals(ax1) #plot arrivals on displacement plot
plot_arrivals(ax2) #plot arrivals on velocity plot
plot_arrivals(ax3) #plot arrivals on acceleration plot
# set up some plot details
ax1.set_ylabel("Vert. Disp, m")
ax2.set_ylabel("Vert. Vel, m/s")
ax3.set_ylabel("Vert. Acc, m/s/s")
ax4.set_ylabel("Vel. Freq, Hz")
ax5.set_ylabel("Vel. PSD, dB")
# get the limits of the y axis so text can be consistently placed
ax4b, ax4t = ax4.get_ylim()
ax4.text(2, ax4t*0.85, 'Plot Start Time: '+start.strftime(' %d/%m/%Y %H:%M:%S.%f UTC (')+str(delay)+' seconds after event).', size='small') # explain difference in x time scale
#adjust subplots for readability
plt.subplots_adjust(hspace=0.7, wspace=0.1, left=0.05, right=0.95, bottom=0.05)
#plot the ray paths
arrivals = model.get_ray_paths(depth, great_angle_deg, phase_list=['ttbasic']) #calculate the ray paths
ax7 = arrivals.plot_rays(plot_type='spherical', ax=ax7, fig=fig, phase_list=['ttbasic'], show=False, legend=True) #plot the ray paths
# Annotate regions
ax7.text(0, 0, 'Solid\ninner\ncore',
horizontalalignment='center', verticalalignment='center',
bbox=dict(facecolor='white', edgecolor='none', alpha=0.7))
ocr = (model.model.radius_of_planet -
(model.model.s_mod.v_mod.iocb_depth +
model.model.s_mod.v_mod.cmb_depth) / 2)
ax7.text(math.radians(180), ocr, 'Fluid outer core',
horizontalalignment='center',
bbox=dict(facecolor='white', edgecolor='none', alpha=0.7))
mr = model.model.radius_of_planet - model.model.s_mod.v_mod.cmb_depth / 2
ax7.text(math.radians(180), mr, 'Solid mantle',
horizontalalignment='center',
bbox=dict(facecolor='white', edgecolor='none', alpha=0.7))
# save the final figure
#plt.savefig('E:\Pictures\Raspberry Shake and Boom\M'+str(mag)+'Quake '+locE+eventID+eventTime.strftime('%Y%m%d %H%M%S UTC.png')) #comment this line out till figure is final
# show the final figure
plt.show()
Lines 131 and 132 are where I try to add the secondary axis to the first subplot ax1.
Lines 137 and 138 are currently to test that the functions for the time conversion work correctly.
FWIW here are the errors I am getting:
Exception in Tkinter callback Traceback (most recent call last): File "F:\Program Files\Python310\lib\tkinter\__init__.py", line 1921, in __call__ return self.func(*args) File "F:\Program Files\Python310\lib\tkinter\__init__.py", line 839, in callit func(*args) File "F:\Program Files\Python310\lib\site-packages\matplotlib\backends\_backend_tk.py", line 251, in idle_draw self.draw() File "F:\Program Files\Python310\lib\site-packages\matplotlib\backends\backend_tkagg.py", line 9, in draw super().draw() File "F:\Program Files\Python310\lib\site-packages\matplotlib\backends\backend_agg.py", line 436, in draw self.figure.draw(self.renderer) File "F:\Program Files\Python310\lib\site-packages\matplotlib\artist.py", line 73, in draw_wrapper result = draw(artist, renderer, *args, **kwargs) File "F:\Program Files\Python310\lib\site-packages\matplotlib\artist.py", line 50, in draw_wrapper return draw(artist, renderer) File "F:\Program Files\Python310\lib\site-packages\matplotlib\figure.py", line 2796, in draw artists = self._get_draw_artists(renderer) File "F:\Program Files\Python310\lib\site-packages\matplotlib\figure.py", line 245, in _get_draw_artists child.apply_aspect(pos) File "F:\Program Files\Python310\lib\site-packages\matplotlib\axes\_secondary_axes.py", line 124, in apply_aspect self._set_lims() File "F:\Program Files\Python310\lib\site-packages\matplotlib\axes\_secondary_axes.py", line 226, in _set_lims lims = self._functions[0](np.array(lims)) File "F:\Program Files\Python310\QReport6dev.py", line 28, in time2UTC tUTC = eventTime + a File "F:\Program Files\Python310\lib\site-packages\obspy\core\utcdatetime.py", line 996, in __add__ return UTCDateTime(ns=self._ns + int(round(value * 1e9))) TypeError: type numpy.ndarray doesn't define __round__ method Exception in Tkinter callback Traceback (most recent call last): File "F:\Program Files\Python310\lib\tkinter\__init__.py", line 1921, in __call__ return self.func(*args) File "F:\Program Files\Python310\lib\tkinter\__init__.py", line 839, in callit func(*args) File "F:\Program Files\Python310\lib\site-packages\matplotlib\backends\_backend_tk.py", line 251, in idle_draw self.draw() File "F:\Program Files\Python310\lib\site-packages\matplotlib\backends\backend_tkagg.py", line 9, in draw super().draw() File "F:\Program Files\Python310\lib\site-packages\matplotlib\backends\backend_agg.py", line 436, in draw self.figure.draw(self.renderer) File "F:\Program Files\Python310\lib\site-packages\matplotlib\artist.py", line 73, in draw_wrapper result = draw(artist, renderer, *args, **kwargs) File "F:\Program Files\Python310\lib\site-packages\matplotlib\artist.py", line 50, in draw_wrapper return draw(artist, renderer) File "F:\Program Files\Python310\lib\site-packages\matplotlib\figure.py", line 2796, in draw artists = self._get_draw_artists(renderer) File "F:\Program Files\Python310\lib\site-packages\matplotlib\figure.py", line 245, in _get_draw_artists child.apply_aspect(pos) File "F:\Program Files\Python310\lib\site-packages\matplotlib\axes\_secondary_axes.py", line 124, in apply_aspect self._set_lims() File "F:\Program Files\Python310\lib\site-packages\matplotlib\axes\_secondary_axes.py", line 226, in _set_lims lims = self._functions[0](np.array(lims)) File "F:\Program Files\Python310\QReport6dev.py", line 28, in time2UTC tUTC = eventTime + a File "F:\Program Files\Python310\lib\site-packages\obspy\core\utcdatetime.py", line 996, in __add__ return UTCDateTime(ns=self._ns + int(round(value * 1e9))) TypeError: type numpy.ndarray doesn't define __round__ method