Test Failed
Push — coverage ( 0196fb...b39e1a )
by Ian
05:17
created

build.rsudp.c_plot.Plot.run()   C

Complexity

Conditions 9

Size

Total Lines 40
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

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