Passed
Push — master ( 85200b...0acd6f )
by Ian
04:11
created

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

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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