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