Passed
Push — master ( 733c5e...5ed0c8 )
by Ian
04:27
created

build.rsudp.c_plot.Plot._draw_lines()   A

Complexity

Conditions 1

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nop 5
dl 0
loc 20
rs 9.8
c 0
b 0
f 0
1
import os, sys, platform
2
import pkg_resources as pr
3
import time
4
import math
5
import numpy as np
6
from datetime import datetime, timedelta
7
import rsudp.raspberryshake as rs
8
from rsudp import printM, printW, printE
9
import rsudp
10
import linecache
11
sender = 'plot.py'
12
QT = False
13
QtGui = False
14
PhotoImage = False
15
try:		# test for matplotlib and exit if import fails
16
	from matplotlib import use
17
	try:	# no way to know what machines can handle what software, but Tk is more universal
18
		use('Qt5Agg')	# try for Qt because it's better and has less threatening errors
19
		from PyQt5 import QtGui
20
		QT = True
21
	except Exception as e:
22
		printW('Qt import failed. Trying Tk...')
23
		printW('detail: %s' % e, spaces=True)
24
		try:	# fail over to the more reliable Tk
25
			use('TkAgg')
26
			from tkinter import PhotoImage
27
		except Exception as e:
28
			printE('Could not import either Qt or Tk, and the plot module requires at least one of them to run.', sender)
29
			printE('Please make sure either PyQt5 or Tkinter is installed.', sender, spaces=True)
30
			printE('detail: %s'% e, sender, spaces=True)
31
			raise ImportError('Could not import either Qt or Tk, and the plot module requires at least one of them to run')
32
	import matplotlib.pyplot as plt
33
	import matplotlib.dates as mdates
34
	import matplotlib.image as mpimg
35
	from matplotlib import rcParams
36
	from matplotlib.ticker import EngFormatter
37
	rcParams['toolbar'] = 'None'
38
	plt.ion()
39
	MPL = True
40
41
	# avoiding a matplotlib user warning
42
	import warnings
43
	warnings.filterwarnings('ignore', category=UserWarning, module='rsudp')
44
45
except Exception as e:
46
	printE('Could not import matplotlib, plotting will not be available.', sender)
47
	printE('detail: %s' % e, sender, spaces=True)
48
	MPL = False
49
50
icon = 'icon.ico'
51
icon2 = 'icon.png'
52
53
class Plot:
54
	'''
55
	.. role:: json(code)
56
		:language: json
57
58
	GUI plotting algorithm, compatible with both Qt5 and TkAgg backends (see :py:func:`matplotlib.use`).
59
	This module can plot seismogram data from a list of 1-4 Shake channels, and calculate and display a spectrogram beneath each.
60
61
	By default the plotted :json:`"duration"` in seconds is :json:`30`.
62
	The plot will refresh at most once per second, but slower processors may take longer.
63
	The longer the duration, the more processor power it will take to refresh the plot,
64
	especially when the spectrogram is enabled.
65
	To disable the spectrogram, set :json:`"spectrogram"` to :json:`false` in the settings file.
66
	To put the plot into fullscreen window mode, set :json:`"fullscreen"` to :json:`true`.
67
	To put the plot into kiosk mode, set :json:`"kiosk"` to :json:`true`.
68
69
	:param cha: channels to plot. Defaults to "all" but can be passed a list of channel names as strings.
70
	:type cha: str or list
71
	:param int seconds: number of seconds to plot. Defaults to 30.
72
	:param bool spectrogram: whether to plot the spectrogram. Defaults to True.
73
	:param bool fullscreen: whether to plot in a fullscreen window. Defaults to False.
74
	:param bool kiosk: whether to plot in kiosk mode (true fullscreen). Defaults to False.
75
	:param deconv: whether to deconvolve the signal. Defaults to False.
76
	:type deconv: str or bool
77
	:param bool screencap: whether or not to save screenshots of events. Defaults to False.
78
	:param bool alert: whether to draw the number of events at startup. Defaults to True.
79
	:param queue.Queue q: queue of data and messages sent by :class:`rsudp.c_consumer.Consumer`
80
	:raise ImportError: if the module cannot import either of the Matplotlib Qt5 or TkAgg backends
81
82
	'''
83
84
	def __init__(self, cha='all', q=False,
85
				 seconds=30, spectrogram=True,
86
				 fullscreen=False, kiosk=False,
87
				 deconv=False, screencap=False,
88
				 alert=True):
89
		"""
90
		Initialize the plot process.
91
92
		"""
93
		super().__init__()
94
		self.sender = 'Plot'
95
		self.alive = True
96
		self.alarm = False			# don't touch this
97
		self.alarm_reset = False	# don't touch this
98
99
		if MPL == False:
100
			sys.stdout.flush()
101
			sys.exit()
102
		if QT == False:
103
			printW('Running on %s machine, using Tk instead of Qt' % (platform.machine()), self.sender)
104
		if q:
105
			self.queue = q
106
		else:
107
			printE('no queue passed to consumer! Thread will exit now!', self.sender)
108
			sys.stdout.flush()
109
			sys.exit()
110
111
		self.master_queue = None	# careful with this, this goes directly to the master consumer. gets set by main thread.
112
113
		self.stream = rs.Stream()
114
		self.raw = rs.Stream()
115
		self.stn = rs.stn
116
		self.net = rs.net
117
		self.chans = []
118
		cha = rs.chns if (cha == 'all') else cha
119
		cha = list(cha) if isinstance(cha, str) else cha
120
		l = rs.chns
121
		for c in l:
122
			n = 0
123
			for uch in cha:
124
				if (uch.upper() in c) and (c not in str(self.chans)):
125
					self.chans.append(c)
126
				n += 1
127
		if len(self.chans) < 1:
128
			self.chans = rs.chns
129
		printM('Plotting channels: %s' % self.chans, self.sender)
130
		self.totchns = rs.numchns
131
		self.seconds = seconds
132
		self.pkts_in_period = rs.tr * rs.numchns * self.seconds	# theoretical number of packets received in self.seconds
133
		self.spectrogram = spectrogram
134
135
		self.deconv = deconv if (deconv in rs.UNITS) else False
136
		if self.deconv and rs.inv:
137
			deconv = deconv.upper()
138
			if self.deconv in rs.UNITS:
139
				self.units = rs.UNITS[self.deconv][0]
140
				self.unit = rs.UNITS[self.deconv][1]
141
			printM('Signal deconvolution set to %s' % (self.deconv), self.sender)
142
		else:
143
			self.units = rs.UNITS['CHAN'][0]
144
			self.unit = rs.UNITS['CHAN'][1]
145
			self.deconv = False
146
		printM('Seismogram units are %s' % (self.units), self.sender)
147
148
		self.per_lap = 0.9
149
		self.fullscreen = fullscreen
150
		self.kiosk = kiosk
151
		self.num_chans = len(self.chans)
152
		self.delay = rs.tr if (self.spectrogram) else 1
153
		self.delay = 0.5 if (self.chans == ['SHZ']) else self.delay
154
155
		self.screencap = screencap
156
		self.save_timer = 0
157
		self.save_pct = 0.7
158
		self.save = []
159
		self.events = 0
160
		self.event_text = ' - detected events: 0' if alert else ''
161
		self.last_event = []
162
		self.last_event_str = False
163
		# plot stuff
164
		self.bgcolor = '#202530' # background
165
		self.fgcolor = '0.8' # axis and label color
166
		self.linecolor = '#c28285' # seismogram color
167
168
		printM('Starting.', self.sender)
169
170
	def deconvolve(self):
171
		'''
172
		Send the streams to the central library deconvolve function.
173
		'''
174
		rs.deconvolve(self)
175
176
	def getq(self):
177
		'''
178
		Get data from the queue and test for whether it has certain strings.
179
		ALARM and TERM both trigger specific behavior.
180
		ALARM messages cause the event counter to increment, and if
181
		:py:data:`screencap==True` then aplot image will be saved when the
182
		event is :py:data:`self.save_pct` of the way across the plot.
183
		'''
184
		d = self.queue.get()
185
		self.queue.task_done()
186
		if 'TERM' in str(d):
187
			plt.close()
188
			if 'SELF' in str(d):
189
				printM('Plot has been closed, plot thread will exit.', self.sender)
190
			self.alive = False
191
			rs.producer = False
192
193
		elif 'ALARM' in str(d):
194
			self.events += 1		# add event to count
195
			self.save_timer -= 1	# don't push the save time forward if there are a large number of alarm events
196
			event = [self.save_timer + int(self.save_pct*self.pkts_in_period),
197
					 rs.fsec(rs.get_msg_time(d))]	# event = [save after count, datetime]
198
			self.last_event_str = '%s UTC' % (event[1].strftime('%Y-%m-%d %H:%M:%S.%f')[:22])
199
			printM('Event time: %s' % (self.last_event_str), sender=self.sender)		# show event time in the logs
200
			if self.screencap:
201
				printM('Saving png in about %i seconds' % (self.save_pct * (self.seconds)), self.sender)
202
				self.save.append(event) # append 
203
			self.fig.suptitle('%s.%s live output - detected events: %s' # title
204
							% (self.net, self.stn, self.events),
205
							fontsize=14, color=self.fgcolor, x=0.52)
206
			self.fig.canvas.set_window_title('(%s) %s.%s - Raspberry Shake Monitor' % (self.events, self.net, self.stn))
207
208
		if rs.getCHN(d) in self.chans:
209
			self.raw = rs.update_stream(
210
				stream=self.raw, d=d, fill_value='latest')
211
			return True
212
		else:
213
			return False
214
		
215
	def set_sps(self):
216
		'''
217
		Get samples per second from the main library.
218
		'''
219
		self.sps = rs.sps
220
221
	# from https://docs.obspy.org/_modules/obspy/imaging/spectrogram.html#_nearest_pow_2:
222
	def _nearest_pow_2(self, x):
223
		"""
224
		Find power of two nearest to x
225
226
		>>> _nearest_pow_2(3)
227
		2.0
228
		>>> _nearest_pow_2(15)
229
		16.0
230
231
		:type x: float
232
		:param x: Number
233
		:rtype: Int
234
		:return: Nearest power of 2 to x
235
236
		Adapted from the `obspy <https://obspy.org>`_ library
237
		"""
238
		a = math.pow(2, math.ceil(np.log2(x)))
239
		b = math.pow(2, math.floor(np.log2(x)))
240
		if abs(a - x) < abs(b - x):
241
			return a
242
		else:
243
			return b
244
245
	def handle_close(self, evt):
246
		'''
247
		Handles a plot close event.
248
		This will trigger a full shutdown of all other processes related to rsudp.
249
		'''
250
		self.master_queue.put(rs.msg_term())
251
252
	def handle_resize(self, evt=False):
253
		'''
254
		Handles a plot window resize event.
255
		This will allow the plot to resize dynamically.
256
		'''
257
		if evt:
258
			h = evt.height
259
		else:
260
			h = self.fig.get_size_inches()[1]*self.fig.dpi
261
		plt.tight_layout(pad=0, h_pad=0.1, w_pad=0,
262
					rect=[0.02, 0.01, 0.98, 0.90 + 0.045*(h/1080)])	# [left, bottom, right, top]
263
264
	def _eventsave(self):
265
		'''
266
		This function takes the next event in line and pops it out of the list,
267
		so that it can be saved and others preserved.
268
		Then, it sets the title to something having to do with the event,
269
		then calls the save figure function, and finally resets the title.
270
		'''
271
		self.save.reverse()
272
		event = self.save.pop()
273
		self.save.reverse()
274
275
		event_time_str = event[1].strftime('%Y-%m-%d-%H%M%S')				# event time for filename
276
		title_time_str = event[1].strftime('%Y-%m-%d %H:%M:%S.%f')[:22]		# pretty event time for plot
277
278
		# change title (just for a moment)
279
		self.fig.suptitle('%s.%s detected event - %s UTC' # title
280
						  % (self.net, self.stn, title_time_str),
281
						  fontsize=14, color=self.fgcolor, x=0.52)
282
283
		# save figure
284
		self.savefig(event_time=event[1], event_time_str=event_time_str)
285
286
		# reset title
287
		self._set_fig_title()
288
289
290
	def savefig(self, event_time=rs.UTCDateTime.now(),
291
				event_time_str=rs.UTCDateTime.now().strftime('%Y-%m-%d-%H%M%S')):
292
		'''
293
		Saves the figure and puts an IMGPATH message on the master queue.
294
		This message can be used to upload the image to various services.
295
296
		:param obspy.core.utcdatetime.UTCDateTime event_time: Event time as an obspy UTCDateTime object.
297
		:param str event_time_str: Event time as a string. This is used to set the filename.
298
		'''
299
		figname = os.path.join(rsudp.scap_dir, '%s-%s.png' % (self.stn, event_time_str))
300
		elapsed = rs.UTCDateTime.now() - event_time
301
		if int(elapsed) > 0:
302
			printM('Saving png %i seconds after alarm' % (elapsed), sender=self.sender)
303
		plt.savefig(figname, facecolor=self.fig.get_facecolor(), edgecolor='none')
304
		printM('Saved %s' % (figname), sender=self.sender)
305
		printM('%s thread has saved an image, sending IMGPATH message to queues' % self.sender, sender=self.sender)
306
		# imgpath requires a UTCDateTime and a string figure path
307
		self.master_queue.put(rs.msg_imgpath(event_time, figname))
308
309
	def _set_fig_title(self):
310
		'''
311
		Sets the figure title back to something that makes sense for the live viewer.
312
		'''
313
		self.fig.suptitle('%s.%s live output - detected events: %s' # title
314
						  % (self.net, self.stn, self.events),
315
						  fontsize=14, color=self.fgcolor, x=0.52)
316
317
318
	def setup_plot(self):
319
		"""
320
		Sets up the plot. Quite a lot of stuff happens in this function.
321
		Matplotlib backends are not threadsafe, so things are a little weird.
322
		See code comments for details.
323
		"""
324
		# instantiate a figure and set basic params
325
		self.fig = plt.figure(figsize=(11,3*self.num_chans))
326
		self.fig.canvas.mpl_connect('close_event', self.handle_close)
327
		self.fig.canvas.mpl_connect('resize_event', self.handle_resize)
328
		
329
		if QT:
330
			self.fig.canvas.window().statusBar().setVisible(False) # remove bottom bar
331
		self.fig.canvas.set_window_title('%s.%s - Raspberry Shake Monitor' % (self.net, self.stn))
332
		self.fig.patch.set_facecolor(self.bgcolor)	# background color
333
		self.fig.suptitle('%s.%s live output%s'	# title
334
						  % (self.net, self.stn, self.event_text),
335
						  fontsize=14, color=self.fgcolor,x=0.52)
336
		self.ax, self.lines = [], []				# list for subplot axes and lines artists
337
		self.mult = 1					# spectrogram selection multiplier
338
		if self.spectrogram:
339
			self.mult = 2				# 2 if user wants a spectrogram else 1
340
			if self.seconds > 60:
341
				self.per_lap = 0.9		# if axis is long, spectrogram overlap can be shorter
342
			else:
343
				self.per_lap = 0.975	# if axis is short, increase resolution
344
			# set spectrogram parameters
345
			self.nfft1 = self._nearest_pow_2(self.sps)
346
			self.nlap1 = self.nfft1 * self.per_lap
347
348
		for i in range(self.num_chans):
349
			if i == 0:
350
				# set up first axes (axes added later will share these x axis limits)
351
				self.ax.append(self.fig.add_subplot(self.num_chans*self.mult,
352
							   1, 1, label=str(1)))
353
				self.ax[0].set_facecolor(self.bgcolor)
354
				self.ax[0].tick_params(colors=self.fgcolor, labelcolor=self.fgcolor)
355
				self.ax[0].xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
356
				self.ax[0].yaxis.set_major_formatter(EngFormatter(unit='%s' % self.unit.lower()))
357
				if self.spectrogram:
358
					self.ax.append(self.fig.add_subplot(self.num_chans*self.mult,
359
								   1, 2, label=str(2)))#, sharex=ax[0]))
360
					self.ax[1].set_facecolor(self.bgcolor)
361
					self.ax[1].tick_params(colors=self.fgcolor, labelcolor=self.fgcolor)
362
			else:
363
				# add axes that share either lines or spectrogram axis limits
364
				s = i * self.mult	# plot selector
365
				# add a subplot then set colors
366
				self.ax.append(self.fig.add_subplot(self.num_chans*self.mult,
367
							   1, s+1, sharex=self.ax[0], label=str(s+1)))
368
				self.ax[s].set_facecolor(self.bgcolor)
369
				self.ax[s].tick_params(colors=self.fgcolor, labelcolor=self.fgcolor)
370
				self.ax[s].xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
371
				self.ax[s].yaxis.set_major_formatter(EngFormatter(unit='%s' % self.unit.lower()))
372
				if self.spectrogram:
373
					# add a spectrogram and set colors
374
					self.ax.append(self.fig.add_subplot(self.num_chans*self.mult,
375
								   1, s+2, sharex=self.ax[1], label=str(s+2)))
376
					self.ax[s+1].set_facecolor(self.bgcolor)
377
					self.ax[s+1].tick_params(colors=self.fgcolor, labelcolor=self.fgcolor)
378
379
		for axis in self.ax:
380
			# set the rest of plot colors
381
			plt.setp(axis.spines.values(), color=self.fgcolor)
382
			plt.setp([axis.get_xticklines(), axis.get_yticklines()], color=self.fgcolor)
383
384
		# calculate times
385
		start = np.datetime64(self.stream[0].stats.endtime
386
							  )-np.timedelta64(self.seconds, 's')	# numpy time
387
		end = np.datetime64(self.stream[0].stats.endtime)	# numpy time
388
389
		# rs logos
390
		mgr = plt.get_current_fig_manager()
391
		ico = pr.resource_filename('rsudp', os.path.join('img', icon))
392
		if QT:
393
			mgr.window.setWindowIcon(QtGui.QIcon(ico))
394
		else:
395
			try:
396
				ico = PhotoImage(file=ico)
397
				mgr.window.tk.call('wm', 'iconphoto', mgr.window._w, ico)
398
			except:
399
				printW('Failed to set PNG icon image, trying .ico instead', sender=self.sender)
400
				try:
401
					ico = pr.resource_filename('rsudp', os.path.join('img', icon2))
402
					ico = PhotoImage(file=ico)
403
					mgr.window.tk.call('wm', 'iconphoto', mgr.window._w, ico)
404
				except:
405
					printE('Failed to set window icon.')
406
407
		im = mpimg.imread(pr.resource_filename('rsudp', os.path.join('img', 'version1-01-small.png')))
408
		self.imax = self.fig.add_axes([0.015, 0.944, 0.2, 0.056], anchor='NW') # [left, bottom, right, top]
409
		self.imax.imshow(im, aspect='equal', interpolation='sinc')
410
		self.imax.axis('off')
411
		# set up axes and artists
412
		for i in range(self.num_chans): # create lines objects and modify axes
413
			if len(self.stream[i].data) < int(self.sps*(1/self.per_lap)):
414
				comp = 0				# spectrogram offset compensation factor
415
			else:
416
				comp = (1/self.per_lap)**2	# spectrogram offset compensation factor
417
			r = np.arange(start, end, np.timedelta64(int(1000/self.sps), 'ms'))[-len(
418
						  self.stream[i].data[int(-self.sps*(self.seconds-(comp/2))):-int(self.sps*(comp/2))]):]
419
			mean = int(round(np.mean(self.stream[i].data)))
420
			# add artist to lines list
421
			self.lines.append(self.ax[i*self.mult].plot(r,
422
							  np.nan*(np.zeros(len(r))),
423
							  label=self.stream[i].stats.channel, color=self.linecolor,
424
							  lw=0.45)[0])
425
			# set axis limits
426
			self.ax[i*self.mult].set_xlim(left=start.astype(datetime),
427
										  right=end.astype(datetime))
428
			self.ax[i*self.mult].set_ylim(bottom=np.min(self.stream[i].data-mean)
429
										  -np.ptp(self.stream[i].data-mean)*0.1,
430
										  top=np.max(self.stream[i].data-mean)
431
										  +np.ptp(self.stream[i].data-mean)*0.1)
432
			# we can set line plot labels here, but not imshow labels
433
			ylabel = self.stream[i].stats.units.strip().capitalize() if (' ' in self.stream[i].stats.units) else self.stream[i].stats.units
434
			self.ax[i*self.mult].set_ylabel(ylabel, color=self.fgcolor)
435
			self.ax[i*self.mult].legend(loc='upper left')	# legend and location
436
			if self.spectrogram:		# if the user wants a spectrogram, plot it
437
				# add spectrogram to axes list
438
				sg = self.ax[1].specgram(self.stream[i].data, NFFT=8, pad_to=8,
439
										 Fs=self.sps, noverlap=7, cmap='inferno',
440
										 xextent=(self.seconds-0.5, self.seconds))[0]
441
				self.ax[1].set_xlim(0,self.seconds)
442
				self.ax[i*self.mult+1].set_ylim(0,int(self.sps/2))
443
				self.ax[i*self.mult+1].imshow(np.flipud(sg**(1/float(10))), cmap='inferno',
444
						extent=(self.seconds-(1/(self.sps/float(len(self.stream[i].data)))),
445
								self.seconds,0,self.sps/2), aspect='auto')
446
447
		self.handle_resize()
448
		# update canvas and draw
449
		figManager = plt.get_current_fig_manager()
450
		if self.kiosk:
451
			figManager.full_screen_toggle()
452
		else:
453
			if self.fullscreen:	# set fullscreen
454
				if QT:	# maximizing in Qt
455
					figManager.window.showMaximized()
456
				else:	# maximizing in Tk
457
					figManager.resize(*figManager.window.maxsize())
458
459
460
		plt.draw()									# draw the canvas
461
		self.fig.canvas.start_event_loop(0.005)		# wait for canvas to update
462
		self.handle_resize()
463
464
465
	def _set_ch_specific_label(self, i):
466
		'''
467
		Set the formatter units if the deconvolution is channel-specific.
468
		'''
469
		if self.deconv:
470
			if (self.deconv in 'CHAN'):
471
				ch = self.stream[i].stats.channel
472
				if ('HZ' in ch) or ('HN' in ch) or ('HE' in ch):
473
					unit = rs.UNITS['VEL'][1]
474
				elif ('EN' in ch):
475
					unit = rs.UNITS['ACC'][1]
476
				else:
477
					unit = rs.UNITS['CHAN'][1]
478
				self.ax[i*self.mult].yaxis.set_major_formatter(EngFormatter(unit='%s' % unit.lower()))
479
480
481
	def _draw_lines(self, i, start, end, mean):
482
		'''
483
		Updates the line data in the plot.
484
485
		:param int i: the trace number
486
		:param numpy.datetime64 start: start time of the trace
487
		:param numpy.datetime64 end: end time of the trace
488
		:param float mean: the mean of data in the trace
489
		'''
490
		comp = 1/self.per_lap	# spectrogram offset compensation factor
491
		r = np.arange(start, end, np.timedelta64(int(1000/self.sps), 'ms'))[-len(
492
					self.stream[i].data[int(-self.sps*(self.seconds-(comp/2))):-int(self.sps*(comp/2))]):]
493
		self.lines[i].set_ydata(self.stream[i].data[int(-self.sps*(self.seconds-(comp/2))):-int(self.sps*(comp/2))]-mean)
494
		self.lines[i].set_xdata(r)	# (1/self.per_lap)/2
495
		self.ax[i*self.mult].set_xlim(left=start.astype(datetime)+timedelta(seconds=comp*1.5),
496
										right=end.astype(datetime))
497
		self.ax[i*self.mult].set_ylim(bottom=np.min(self.stream[i].data-mean)
498
										-np.ptp(self.stream[i].data-mean)*0.1,
499
										top=np.max(self.stream[i].data-mean)
500
										+np.ptp(self.stream[i].data-mean)*0.1)
501
502
503
	def _update_specgram(self, i, mean):
504
		'''
505
		Updates the spectrogram and its labels.
506
507
		:param int i: the trace number
508
		:param float mean: the mean of data in the trace
509
		'''
510
		self.nfft1 = self._nearest_pow_2(self.sps)	# FFTs run much faster if the number of transforms is a power of 2
511
		self.nlap1 = self.nfft1 * self.per_lap
512
		if len(self.stream[i].data) < self.nfft1:	# when the number of data points is low, we just need to kind of fake it for a few fractions of a second
513
			self.nfft1 = 8
514
			self.nlap1 = 6
515
		sg = self.ax[i*self.mult+1].specgram(self.stream[i].data-mean,
516
					NFFT=self.nfft1, pad_to=int(self.nfft1*4), # previously self.sps*4),
517
					Fs=self.sps, noverlap=self.nlap1)[0]	# meat & potatoes
518
		self.ax[i*self.mult+1].clear()	# incredibly important, otherwise continues to draw over old images (gets exponentially slower)
519
		# cloogy way to shift the spectrogram to line up with the seismogram
520
		self.ax[i*self.mult+1].set_xlim(0.25,self.seconds-0.25)
521
		self.ax[i*self.mult+1].set_ylim(0,int(self.sps/2))
522
		# imshow to update the spectrogram
523
		self.ax[i*self.mult+1].imshow(np.flipud(sg**(1/float(10))), cmap='inferno',
524
				extent=(self.seconds-(1/(self.sps/float(len(self.stream[i].data)))),
525
						self.seconds,0,self.sps/2), aspect='auto')
526
		# some things that unfortunately can't be in the setup function:
527
		self.ax[i*self.mult+1].tick_params(axis='x', which='both',
528
				bottom=False, top=False, labelbottom=False)
529
		self.ax[i*self.mult+1].set_ylabel('Frequency (Hz)', color=self.fgcolor)
530
		self.ax[i*self.mult+1].set_xlabel('Time (UTC)', color=self.fgcolor)
531
532
533
	def update_plot(self):
534
		'''
535
		Redraw the plot with new data.
536
		Called on every nth loop after the plot is set up, where n is
537
		the number of channels times the data packet arrival rate in Hz.
538
		This has the effect of making the plot update once per second.
539
		'''
540
		obstart = self.stream[0].stats.endtime - timedelta(seconds=self.seconds)	# obspy time
541
		start = np.datetime64(self.stream[0].stats.endtime
542
							  )-np.timedelta64(self.seconds, 's')	# numpy time
543
		end = np.datetime64(self.stream[0].stats.endtime)	# numpy time
544
		self.raw = self.raw.slice(starttime=obstart)	# slice the stream to the specified length (seconds variable)
545
		self.stream = self.stream.slice(starttime=obstart)	# slice the stream to the specified length (seconds variable)
546
		i = 0
547
		for i in range(self.num_chans):	# for each channel, update the plots
548
			mean = int(round(np.mean(self.stream[i].data)))
549
			self._draw_lines(i, start, end, mean)
550
			self._set_ch_specific_label(i)
551
			if self.spectrogram:
552
				self._update_specgram(i, mean)
553
			else:
554
				# also can't be in the setup function
555
				self.ax[i*self.mult].set_xlabel('Time (UTC)', color=self.fgcolor)
556
557
558
	def figloop(self):
559
		"""
560
		Let some time elapse in order for the plot canvas to draw properly.
561
		Must be separate from :py:func:`update_plot()` to avoid a broadcast error early in plotting.
562
		"""
563
		self.fig.canvas.start_event_loop(0.005)
564
565
566
	def mainloop(self, i, u):
567
		'''
568
		The main loop in the :py:func:`rsudp.c_plot.Plot.run`.
569
570
		:param int i: number of plot events without clearing the linecache
571
		:param int u: queue blocking counter
572
		:return: number of plot events without clearing the linecache and queue blocking counter
573
		:rtype: int, int
574
		'''
575
		if i > 10:
576
			linecache.clearcache()
577
			i = 0
578
		else:
579
			i += 1
580
		self.stream = rs.copy(self.stream)	# essential, otherwise the stream has a memory leak
581
		self.raw = rs.copy(self.raw)		# and could eventually crash the machine
582
		self.deconvolve()
583
		self.update_plot()
584
		if u >= 0:				# avoiding a matplotlib broadcast error
585
			self.figloop()
586
587
		if self.save:
588
			# save the plot
589
			if (self.save_timer > self.save[0][0]):
590
				self._eventsave()
591
		u = 0
592
		time.sleep(0.005)		# wait a ms to see if another packet will arrive
593
		sys.stdout.flush()
594
		return i, u
595
596
	def qu(self, u):
597
		'''
598
		Get a queue object and increment the queue counter.
599
		This is a way to figure out how many channels have arrived in the queue.
600
601
		:param int u: queue blocking counter
602
		:return: queue blocking counter
603
		:rtype: int
604
		'''
605
		u += 1 if self.getq() else 0
606
		return u
607
608
609
	def run(self):
610
		"""
611
		The heart of the plotting routine.
612
613
		Begins by updating the queue to populate a :py:class:`obspy.core.stream.Stream` object, then setting up the main plot.
614
		The first time through the main loop, the plot is not drawn. After that, the plot is drawn every time all channels are updated.
615
		Any plots containing a spectrogram and more than 1 channel are drawn at most every second (1000 ms).
616
		All other plots are drawn at most every quarter second (250 ms).
617
		"""
618
		self.getq() # block until data is flowing from the consumer
619
		for i in range((self.totchns)*2): # fill up a stream object
620
			self.getq()
621
		self.set_sps()
622
		self.deconvolve()
623
		self.setup_plot()
624
625
		n = 0	# number of iterations without plotting
626
		i = 0	# number of plot events without clearing the linecache
627
		u = -1	# number of blocked queue calls (must be -1 at startup)
628
		while True: # main loop
629
			while True: # sub loop
630
				if self.alive == False:	# break if the user has closed the plot
631
					break
632
				n += 1
633
				self.save_timer += 1
634
				if self.queue.qsize() > 0:
635
					self.getq()
636
					time.sleep(0.009)		# wait a ms to see if another packet will arrive
637
				else:
638
					u = self.qu(u)
639
					if n > (self.delay * rs.numchns):
640
						n = 0
641
						break
642
			if self.alive == False:	# break if the user has closed the plot
643
				printM('Exiting.', self.sender)
644
				break
645
			i, u = self.mainloop(i, u)
646
		return
647