build.rsudp.c_plot.Plot._format_axes()   B
last analyzed

Complexity

Conditions 5

Size

Total Lines 48
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 5.0018

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 48
ccs 23
cts 24
cp 0.9583
rs 8.5253
c 0
b 0
f 0
cc 5
nop 1
crap 5.0018
1 1
import os, sys, platform
2 1
import pkg_resources as pr
3 1
import time
4 1
import math
5 1
import numpy as np
6 1
from datetime import datetime, timedelta
7 1
import rsudp.raspberryshake as rs
8 1
from rsudp import printM, printW, printE, get_scap_dir, helpers
9 1
from rsudp.test import TEST
10 1
import linecache
11 1
sender = 'plot.py'
12 1
QT = False
13 1
QtGui = False
14 1
PhotoImage = False
15 1
try:		# test for matplotlib and exit if import fails
16 1
	from matplotlib import use
17 1
	try:	# no way to know what machines can handle what software, but Tk is more universal
18 1
		use('Qt5Agg')	# try for Qt because it's better and has less threatening errors
19 1
		from PyQt5 import QtGui
20
		QT = True
21 1
	except Exception as e:
22 1
		printW('Qt import failed. Trying Tk...')
23 1
		printW('detail: %s' % e, spaces=True)
24 1
		try:	# fail over to the more reliable Tk
25 1
			use('TkAgg')
26 1
			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 1
	import matplotlib.pyplot as plt
33 1
	import matplotlib.dates as mdates
34 1
	import matplotlib.image as mpimg
35 1
	from matplotlib import rcParams
36 1
	from matplotlib.ticker import EngFormatter
37 1
	rcParams['toolbar'] = 'None'
38 1
	plt.ion()
39 1
	MPL = True
40
41
	# avoiding a matplotlib user warning
42 1
	import warnings
43 1
	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 1
ICON = 'icon.ico'
51 1
ICON2 = 'icon.png'
52
53 1
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 1
	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 1
		self.deconv = deconv if (deconv in rs.UNITS) else False
97 1
		if self.deconv and rs.inv:
98 1
			deconv = deconv.upper()
99 1
			if self.deconv in rs.UNITS:
100 1
				self.units = rs.UNITS[self.deconv][0]
101 1
				self.unit = rs.UNITS[self.deconv][1]
102 1
			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 1
		printM('Seismogram units are %s' % (self.units), self.sender)
108
109
110 1
	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 1
		super().__init__()
120 1
		self.sender = 'Plot'
121 1
		self.alive = True
122 1
		self.testing = testing
123 1
		self.alarm = False			# don't touch this
124 1
		self.alarm_reset = False	# don't touch this
125
126 1
		if MPL == False:
127
			sys.stdout.flush()
128
			sys.exit()
129 1
		if QT == False:
130 1
			printW('Running on %s machine, using Tk instead of Qt' % (platform.machine()), self.sender)
131
132 1
		self.queue = q
133 1
		self.master_queue = None	# careful with this, this goes directly to the master consumer. gets set by main thread.
134
135 1
		self.stream = rs.Stream()
136 1
		self.raw = rs.Stream()
137 1
		self.stn = rs.stn
138 1
		self.net = rs.net
139
140 1
		self.chans = []
141 1
		helpers.set_channels(self, cha)
142 1
		printM('Plotting %s channels: %s' % (len(self.chans), self.chans), self.sender)
143 1
		self.totchns = rs.numchns
144
145 1
		self.seconds = seconds
146 1
		self.pkts_in_period = rs.tr * rs.numchns * self.seconds	# theoretical number of packets received in self.seconds
147 1
		self.spectrogram = spectrogram
148
149 1
		self._set_deconv(deconv)
150
151 1
		self.per_lap = 0.9
152 1
		self.fullscreen = fullscreen
153 1
		self.kiosk = kiosk
154 1
		self.num_chans = len(self.chans)
155 1
		self.delay = rs.tr if (self.spectrogram) else 1
156 1
		self.delay = 0.5 if (self.chans == ['SHZ']) else self.delay
157
158 1
		self.screencap = screencap
159 1
		self.save_timer = 0
160 1
		self.save_pct = 0.7
161 1
		self.save = []
162 1
		self.events = 0
163 1
		self.event_text = ' - detected events: 0' if alert else ''
164 1
		self.last_event = []
165 1
		self.last_event_str = False
166
		# plot stuff
167 1
		self.bgcolor = '#202530' # background
168 1
		self.fgcolor = '0.8' # axis and label color
169 1
		self.linecolor = '#c28285' # seismogram color
170
171 1
		printM('Starting.', self.sender)
172
173 1
	def deconvolve(self):
174
		'''
175
		Send the streams to the central library deconvolve function.
176
		'''
177 1
		helpers.deconvolve(self)
178
179 1
	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 1
		d = self.queue.get()
188 1
		self.queue.task_done()
189 1
		if 'TERM' in str(d):
190 1
			plt.close()
191 1
			if 'SELF' in str(d):
192
				printM('Plot has been closed, plot thread will exit.', self.sender)
193 1
			self.alive = False
194 1
			rs.producer = False
195
196 1
		elif 'ALARM' in str(d):
197 1
			self.events += 1		# add event to count
198 1
			self.save_timer -= 1	# don't push the save time forward if there are a large number of alarm events
199 1
			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 1
			self.last_event_str = '%s UTC' % (event[1].strftime('%Y-%m-%d %H:%M:%S.%f')[:22])
202 1
			printM('Event time: %s' % (self.last_event_str), sender=self.sender)		# show event time in the logs
203 1
			if self.screencap:
204 1
				printM('Saving png in about %i seconds' % (self.save_pct * (self.seconds)), self.sender)
205 1
				self.save.append(event) # append 
206 1
			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 1
			self.fig.canvas.set_window_title('(%s) %s.%s - Raspberry Shake Monitor' % (self.events, self.net, self.stn))
210
211 1
		if rs.getCHN(d) in self.chans:
212 1
			self.raw = rs.update_stream(
213
				stream=self.raw, d=d, fill_value='latest')
214 1
			return True
215
		else:
216 1
			return False
217
		
218 1
	def set_sps(self):
219
		'''
220
		Get samples per second from the main library.
221
		'''
222 1
		self.sps = rs.sps
223
224
	# from https://docs.obspy.org/_modules/obspy/imaging/spectrogram.html#_nearest_pow_2:
225 1
	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 1
		a = math.pow(2, math.ceil(np.log2(x)))
242 1
		b = math.pow(2, math.floor(np.log2(x)))
243 1
		if abs(a - x) < abs(b - x):
244 1
			return a
245
		else:
246
			return b
247
248 1
	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 1
		self.master_queue.put(helpers.msg_term())
254
255 1
	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 1
		if evt:
261 1
			h = evt.height
262
		else:
263 1
			h = self.fig.get_size_inches()[1]*self.fig.dpi
264 1
		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 1
	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 1
		self.save.reverse()
275 1
		event = self.save.pop()
276 1
		self.save.reverse()
277
278 1
		event_time_str = event[1].strftime('%Y-%m-%d-%H%M%S')				# event time for filename
279 1
		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 1
		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 1
		self.savefig(event_time=event[1], event_time_str=event_time_str)
288
289
		# reset title
290 1
		self._set_fig_title()
291
292
293 1
	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. Defaults to ``UTCDateTime.now()``.
300
		:param str event_time_str: Event time as a string, in the format ``'%Y-%m-%d-%H%M%S'``. This is used to set the filename.
301
		'''
302 1
		figname = os.path.join(get_scap_dir(), '%s-%s.png' % (self.stn, event_time_str))
303 1
		elapsed = rs.UTCDateTime.now() - event_time
304 1
		if int(elapsed) > 0:
305 1
			printM('Saving png %i seconds after alarm' % (elapsed), sender=self.sender)
306 1
		plt.savefig(figname, facecolor=self.fig.get_facecolor(), edgecolor='none')
307 1
		printM('Saved %s' % (figname), sender=self.sender)
308 1
		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 1
		self.master_queue.put(helpers.msg_imgpath(event_time, figname))
311
312
313 1
	def _set_fig_title(self):
314
		'''
315
		Sets the figure title back to something that makes sense for the live viewer.
316
		'''
317 1
		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 1
	def _init_plot(self):
323
		'''
324
		Initialize plot elements and calculate parameters.
325
		'''
326 1
		self.fig = plt.figure(figsize=(11,3*self.num_chans))
327 1
		self.fig.canvas.mpl_connect('close_event', self.handle_close)
328 1
		self.fig.canvas.mpl_connect('resize_event', self.handle_resize)
329
		
330 1
		if QT:
331
			self.fig.canvas.window().statusBar().setVisible(False) # remove bottom bar
332 1
		self.fig.canvas.set_window_title('%s.%s - Raspberry Shake Monitor' % (self.net, self.stn))
333 1
		self.fig.patch.set_facecolor(self.bgcolor)	# background color
334 1
		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 1
		self.ax, self.lines = [], []				# list for subplot axes and lines artists
338 1
		self.mult = 1					# spectrogram selection multiplier
339 1
		if self.spectrogram:
340 1
			self.mult = 2				# 2 if user wants a spectrogram else 1
341 1
			if self.seconds > 60:
342
				self.per_lap = 0.9		# if axis is long, spectrogram overlap can be shorter
343
			else:
344 1
				self.per_lap = 0.975	# if axis is short, increase resolution
345
			# set spectrogram parameters
346 1
			self.nfft1 = self._nearest_pow_2(self.sps)
347 1
			self.nlap1 = self.nfft1 * self.per_lap
348
349
350 1
	def _init_axes(self, i):
351
		'''
352
		Initialize plot axes.
353
		'''
354 1
		if i == 0:
355
			# set up first axes (axes added later will share these x axis limits)
356 1
			self.ax.append(self.fig.add_subplot(self.num_chans*self.mult,
357
							1, 1, label=str(1)))
358 1
			self.ax[0].set_facecolor(self.bgcolor)
359 1
			self.ax[0].tick_params(colors=self.fgcolor, labelcolor=self.fgcolor)
360 1
			self.ax[0].xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
361 1
			self.ax[0].yaxis.set_major_formatter(EngFormatter(unit='%s' % self.unit.lower()))
362 1
			if self.spectrogram:
363 1
				self.ax.append(self.fig.add_subplot(self.num_chans*self.mult,
364
								1, 2, label=str(2)))#, sharex=ax[0]))
365 1
				self.ax[1].set_facecolor(self.bgcolor)
366 1
				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 1
			s = i * self.mult	# plot selector
370
			# add a subplot then set colors
371 1
			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 1
			self.ax[s].set_facecolor(self.bgcolor)
374 1
			self.ax[s].tick_params(colors=self.fgcolor, labelcolor=self.fgcolor)
375 1
			self.ax[s].xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
376 1
			self.ax[s].yaxis.set_major_formatter(EngFormatter(unit='%s' % self.unit.lower()))
377 1
			if self.spectrogram:
378
				# add a spectrogram and set colors
379 1
				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 1
				self.ax[s+1].set_facecolor(self.bgcolor)
382 1
				self.ax[s+1].tick_params(colors=self.fgcolor, labelcolor=self.fgcolor)
383
384
385 1
	def _set_icon(self):
386
		'''
387
		Set RS plot icons.
388
		'''
389 1
		mgr = plt.get_current_fig_manager()
390 1
		ico = pr.resource_filename('rsudp', os.path.join('img', ICON))
391 1
		if QT:
392
			mgr.window.setWindowIcon(QtGui.QIcon(ico))
393
		else:
394 1
			try:
395 1
				ico = PhotoImage(file=ico)
396
				mgr.window.tk.call('wm', 'iconphoto', mgr.window._w, ico)
397 1
			except:
398 1
				printW('Failed to set PNG icon image, trying .ico instead', sender=self.sender)
399 1
				try:
400 1
					ico = pr.resource_filename('rsudp', os.path.join('img', ICON2))
401 1
					ico = PhotoImage(file=ico)
402 1
					mgr.window.tk.call('wm', 'iconphoto', mgr.window._w, ico)
403
				except:
404
					printE('Failed to set window icon.')
405
406
407 1
	def _format_axes(self):
408
		'''
409
		Setting up axes and artists.
410
		'''
411
		# calculate times
412 1
		start = np.datetime64(self.stream[0].stats.endtime
413
							  )-np.timedelta64(self.seconds, 's')	# numpy time
414 1
		end = np.datetime64(self.stream[0].stats.endtime)	# numpy time
415
416 1
		im = mpimg.imread(pr.resource_filename('rsudp', os.path.join('img', 'version1-01-small.png')))
417 1
		self.imax = self.fig.add_axes([0.015, 0.944, 0.2, 0.056], anchor='NW') # [left, bottom, right, top]
418 1
		self.imax.imshow(im, aspect='equal', interpolation='sinc')
419 1
		self.imax.axis('off')
420
		# set up axes and artists
421 1
		for i in range(self.num_chans): # create lines objects and modify axes
422 1
			if len(self.stream[i].data) < int(self.sps*(1/self.per_lap)):
423 1
				comp = 0				# spectrogram offset compensation factor
424
			else:
425
				comp = (1/self.per_lap)**2	# spectrogram offset compensation factor
426 1
			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 1
			mean = int(round(np.mean(self.stream[i].data)))
429
			# add artist to lines list
430 1
			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 1
			self.ax[i*self.mult].set_xlim(left=start.astype(datetime),
436
										  right=end.astype(datetime))
437 1
			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 1
			ylabel = self.stream[i].stats.units.strip().capitalize() if (' ' in self.stream[i].stats.units) else self.stream[i].stats.units
443 1
			self.ax[i*self.mult].set_ylabel(ylabel, color=self.fgcolor)
444 1
			self.ax[i*self.mult].legend(loc='upper left')	# legend and location
445 1
			if self.spectrogram:		# if the user wants a spectrogram, plot it
446
				# add spectrogram to axes list
447 1
				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 1
				self.ax[1].set_xlim(0,self.seconds)
451 1
				self.ax[i*self.mult+1].set_ylim(0,int(self.sps/2))
452 1
				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 1
	def _setup_fig_manager(self):
458
		'''
459
		Setting up figure manager and 
460
		'''
461
		# update canvas and draw
462 1
		figManager = plt.get_current_fig_manager()
463 1
		if self.kiosk:
464
			figManager.full_screen_toggle()
465
		else:
466 1
			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 1
	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 1
		self._init_plot()
481
482 1
		for i in range(self.num_chans):
483 1
			self._init_axes(i)
484
485 1
		for axis in self.ax:
486
			# set the rest of plot colors
487 1
			plt.setp(axis.spines.values(), color=self.fgcolor)
488 1
			plt.setp([axis.get_xticklines(), axis.get_yticklines()], color=self.fgcolor)
489
490
		# rs logos
491 1
		self._set_icon()
492
493
		# draw axes
494 1
		self._format_axes()
495
496 1
		self.handle_resize()
497
498
		# setup figure manager
499 1
		self._setup_fig_manager()
500
501
		# draw plot, loop, and resize the plot
502 1
		plt.draw()									# draw the canvas
503 1
		self.fig.canvas.start_event_loop(0.005)		# wait for canvas to update
504 1
		self.handle_resize()
505
506
507 1
	def _set_ch_specific_label(self, i):
508
		'''
509
		Set the formatter units if the deconvolution is channel-specific.
510
		'''
511 1
		if self.deconv:
512 1
			if (self.deconv in 'CHAN'):
513 1
				ch = self.stream[i].stats.channel
514 1
				if ('HZ' in ch) or ('HN' in ch) or ('HE' in ch):
515 1
					unit = rs.UNITS['VEL'][1]
516 1
				elif ('EN' in ch):
517 1
					unit = rs.UNITS['ACC'][1]
518
				else:
519
					unit = rs.UNITS['CHAN'][1]
520 1
				self.ax[i*self.mult].yaxis.set_major_formatter(EngFormatter(unit='%s' % unit.lower()))
521
522
523 1
	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 1
		comp = 1/self.per_lap	# spectrogram offset compensation factor
533 1
		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 1
		self.lines[i].set_ydata(self.stream[i].data[int(-self.sps*(self.seconds-(comp/2))):-int(self.sps*(comp/2))]-mean)
536 1
		self.lines[i].set_xdata(r)	# (1/self.per_lap)/2
537 1
		self.ax[i*self.mult].set_xlim(left=start.astype(datetime)+timedelta(seconds=comp*1.5),
538
										right=end.astype(datetime))
539 1
		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 1
	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 1
		self.nfft1 = self._nearest_pow_2(self.sps)	# FFTs run much faster if the number of transforms is a power of 2
553 1
		self.nlap1 = self.nfft1 * self.per_lap
554 1
		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 1
		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 1
		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 1
		self.ax[i*self.mult+1].set_xlim(0.25,self.seconds-0.25)
563 1
		self.ax[i*self.mult+1].set_ylim(0,int(self.sps/2))
564
		# imshow to update the spectrogram
565 1
		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 1
		self.ax[i*self.mult+1].tick_params(axis='x', which='both',
570
				bottom=False, top=False, labelbottom=False)
571 1
		self.ax[i*self.mult+1].set_ylabel('Frequency (Hz)', color=self.fgcolor)
572 1
		self.ax[i*self.mult+1].set_xlabel('Time (UTC)', color=self.fgcolor)
573
574
575 1
	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 1
		obstart = self.stream[0].stats.endtime - timedelta(seconds=self.seconds)	# obspy time
583 1
		start = np.datetime64(self.stream[0].stats.endtime
584
							  )-np.timedelta64(self.seconds, 's')	# numpy time
585 1
		end = np.datetime64(self.stream[0].stats.endtime)	# numpy time
586 1
		self.raw = self.raw.slice(starttime=obstart)	# slice the stream to the specified length (seconds variable)
587 1
		self.stream = self.stream.slice(starttime=obstart)	# slice the stream to the specified length (seconds variable)
588 1
		i = 0
589 1
		for i in range(self.num_chans):	# for each channel, update the plots
590 1
			mean = int(round(np.mean(self.stream[i].data)))
591 1
			self._draw_lines(i, start, end, mean)
592 1
			self._set_ch_specific_label(i)
593 1
			if self.spectrogram:
594 1
				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 1
	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 1
		self.fig.canvas.start_event_loop(0.005)
606
607
608 1
	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 1
		if i > 10:
618 1
			linecache.clearcache()
619 1
			i = 0
620
		else:
621 1
			i += 1
622 1
		self.stream = rs.copy(self.stream)	# essential, otherwise the stream has a memory leak
623 1
		self.raw = rs.copy(self.raw)		# and could eventually crash the machine
624 1
		self.deconvolve()
625 1
		self.update_plot()
626 1
		if u >= 0:				# avoiding a matplotlib broadcast error
627 1
			self.figloop()
628
629 1
		if self.save:
630
			# save the plot
631 1
			if (self.save_timer > self.save[0][0]):
632 1
				self._eventsave()
633 1
		u = 0
634 1
		time.sleep(0.005)		# wait a ms to see if another packet will arrive
635 1
		sys.stdout.flush()
636 1
		return i, u
637
638 1
	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 1
		u += 1 if self.getq() else 0
648 1
		return u
649
650
651 1
	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 1
		self.getq() # block until data is flowing from the consumer
661 1
		for i in range((self.totchns)*2): # fill up a stream object
662 1
			self.getq()
663 1
		self.set_sps()
664 1
		self.deconvolve()
665 1
		self.setup_plot()
666
667 1
		n = 0	# number of iterations without plotting
668 1
		i = 0	# number of plot events without clearing the linecache
669 1
		u = -1	# number of blocked queue calls (must be -1 at startup)
670 1
		while True: # main loop
671 1
			while True: # sub loop
672 1
				if self.alive == False:	# break if the user has closed the plot
673 1
					break
674 1
				n += 1
675 1
				self.save_timer += 1
676 1
				if self.queue.qsize() > 0:
677 1
					self.getq()
678 1
					time.sleep(0.009)		# wait a ms to see if another packet will arrive
679
				else:
680 1
					u = self.qu(u)
681 1
					if n > (self.delay * rs.numchns):
682 1
						n = 0
683 1
						break
684 1
			if self.alive == False:	# break if the user has closed the plot
685 1
				printM('Exiting.', self.sender)
686 1
				break
687 1
			i, u = self.mainloop(i, u)
688 1
			if self.testing:
689 1
				TEST['c_plot'][1] = True
690
		return
691