[update] added possibility to modify voltage level in plots from settings in parameters.yaml

This commit is contained in:
Marcel Paffrath 2022-11-21 15:31:32 +01:00
parent 3fe5fc48d1
commit a6d59c8c71
5 changed files with 119 additions and 38 deletions

View File

@ -55,3 +55,15 @@ EMAIL:
addresses: ['marcel.paffrath@rub.de', 'kasper.fischer@rub.de'] # list of mail addresses for info mails addresses: ['marcel.paffrath@rub.de', 'kasper.fischer@rub.de'] # list of mail addresses for info mails
sender: 'webmaster@geophysik.ruhr-uni-bochum.de' # mail sender sender: 'webmaster@geophysik.ruhr-uni-bochum.de' # mail sender
# Factor for channel to SI-units (for plotting)
CHANNEL_UNITS:
EX1: 1e-6
EX2: 1e-6
EX3: 1e-6
VEI: 1e-3
# Transform channel for plotting, perform arithmetic operations in given order, e.g.: PBox EX1 V to deg C: 20 * x -20
CHANNEL_TRANSFORM:
EX1:
- ['*', 20]
- ['-', 20]

View File

@ -17,13 +17,14 @@ import matplotlib.pyplot as plt
from obspy import read, UTCDateTime, Stream from obspy import read, UTCDateTime, Stream
from obspy.clients.filesystem.sds import Client from obspy.clients.filesystem.sds import Client
from write_utils import write_html_text, write_html_row, write_html_footer, write_html_header, get_print_title_str,\ from write_utils import write_html_text, write_html_row, write_html_footer, write_html_header, get_print_title_str, \
init_html_table, finish_html_table init_html_table, finish_html_table
from utils import get_bg_color from utils import get_bg_color, modify_stream_for_plot
try: try:
import smtplib import smtplib
from email.mime.text import MIMEText from email.mime.text import MIMEText
mail_functionality = True mail_functionality = True
except ImportError: except ImportError:
print('Could not import smtplib or mail. Disabled sending mails.') print('Could not import smtplib or mail. Disabled sending mails.')
@ -188,7 +189,7 @@ class SurveillanceBot(object):
stream = self.data.get(nwst_id) stream = self.data.get(nwst_id)
if stream: if stream:
nsl = nsl_from_id(nwst_id) nsl = nsl_from_id(nwst_id)
station_qc = StationQC(stream, nsl, self.parameters, self.keys, qc_starttime, station_qc = StationQC(self, stream, nsl, self.parameters, self.keys, qc_starttime,
self.verbosity, print_func=self.print, self.verbosity, print_func=self.print,
status_track=self.status_track.get(nwst_id)) status_track=self.status_track.get(nwst_id))
analysis_print_result = station_qc.return_print_analysis() analysis_print_result = station_qc.return_print_analysis()
@ -277,7 +278,7 @@ class SurveillanceBot(object):
if self.outpath_html: if self.outpath_html:
self.write_html_table() self.write_html_table()
if self.parameters.get('html_figures'): if self.parameters.get('html_figures'):
self.write_html_figures(check_plot_time=not(first_exec)) self.write_html_figures(check_plot_time=not (first_exec))
else: else:
self.print_analysis() self.print_analysis()
time.sleep(self.refresh_period) time.sleep(self.refresh_period)
@ -321,21 +322,28 @@ class SurveillanceBot(object):
os.mkdir(self.outpath_html) os.mkdir(self.outpath_html)
def write_html_figures(self, check_plot_time=True): def write_html_figures(self, check_plot_time=True):
""" Write figures for html, right now hardcoded hourly """ """ Write figures for html (e.g. hourly) """
if check_plot_time and not self.check_plot_hour(): if check_plot_time and not self.check_plot_hour():
return return
self.check_fig_dir()
for nwst_id in self.station_list: for nwst_id in self.station_list:
fig = plt.figure(figsize=(16, 9)) self.write_html_figure(nwst_id)
fnout = self.get_fig_path_abs(nwst_id)
st = self.data.get(nwst_id) def write_html_figure(self, nwst_id):
if st: """ Write figure for html for specified station """
st.plot(fig=fig, show=False, draw=False, block=False, equal_scale=False, method='full') self.check_fig_dir()
ax = fig.axes[0]
ax.set_title(f'Hourly refreshed plot at (UTC) {UTCDateTime.now().strftime("%Y-%m-%d %H:%M:%S")}') fig = plt.figure(figsize=(16, 9))
fig.savefig(fnout, dpi=150., bbox_inches='tight') fnout = self.get_fig_path_abs(nwst_id)
plt.close(fig) st = self.data.get(nwst_id)
if st:
st = modify_stream_for_plot(st, parameters=self.parameters)
st.plot(fig=fig, show=False, draw=False, block=False, equal_scale=False, method='full')
ax = fig.axes[0]
ax.set_title(f'Plot refreshed at (UTC) {UTCDateTime.now().strftime("%Y-%m-%d %H:%M:%S")}. '
f'Refreshed hourly or on FAIL status.')
fig.savefig(fnout, dpi=150., bbox_inches='tight')
plt.close(fig)
def write_html_table(self, default_color='#e6e6e6'): def write_html_table(self, default_color='#e6e6e6'):
self.check_html_dir() self.check_html_dir()
@ -345,7 +353,7 @@ class SurveillanceBot(object):
try: try:
with open(fnout, 'w') as outfile: with open(fnout, 'w') as outfile:
write_html_header(outfile, self.refresh_period) write_html_header(outfile, self.refresh_period)
#write_html_table_title(outfile, self.parameters) # write_html_table_title(outfile, self.parameters)
init_html_table(outfile) init_html_table(outfile)
# First write header items # First write header items
@ -405,7 +413,7 @@ class SurveillanceBot(object):
timespan = timedelta(seconds=int(self.parameters.get('timespan') * 24 * 3600)) timespan = timedelta(seconds=int(self.parameters.get('timespan') * 24 * 3600))
self.status_message = f'Program starttime (UTC) {self.starttime.strftime("%Y-%m-%d %H:%M:%S")} | ' \ self.status_message = f'Program starttime (UTC) {self.starttime.strftime("%Y-%m-%d %H:%M:%S")} | ' \
f'Current time (UTC) {UTCDateTime().strftime("%Y-%m-%d %H:%M:%S")} | ' \ f'Current time (UTC) {UTCDateTime().strftime("%Y-%m-%d %H:%M:%S")} | ' \
f'Refresh period: {self.refresh_period}s | '\ f'Refresh period: {self.refresh_period}s | ' \
f'Showing data of last {timespan}' f'Showing data of last {timespan}'
def print(self, string, **kwargs): def print(self, string, **kwargs):
@ -422,12 +430,13 @@ class SurveillanceBot(object):
class StationQC(object): class StationQC(object):
def __init__(self, stream, nsl, parameters, keys, starttime, verbosity, print_func, status_track={}): def __init__(self, parent, stream, nsl, parameters, keys, starttime, verbosity, print_func, status_track={}):
""" """
Station Quality Check class. Station Quality Check class.
:param nsl: dictionary containing network, station and location (key: str) :param nsl: dictionary containing network, station and location (key: str)
:param parameters: parameters dictionary from parameters.yaml file :param parameters: parameters dictionary from parameters.yaml file
""" """
self.parent = parent
self.stream = stream self.stream = stream
self.nsl = nsl self.nsl = nsl
self.network = nsl.get('network') self.network = nsl.get('network')
@ -448,6 +457,10 @@ class StationQC(object):
self.start() self.start()
@property
def nwst_id(self):
return f'{self.network}.{self.station}'
def status_ok(self, key, detailed_message="Everything OK", status_message='OK', overwrite=False): def status_ok(self, key, detailed_message="Everything OK", status_message='OK', overwrite=False):
current_status = self.status_dict.get(key) current_status = self.status_dict.get(key)
# do not overwrite existing warnings or errors # do not overwrite existing warnings or errors
@ -497,6 +510,9 @@ class StationQC(object):
current_status.count += count current_status.count += count
else: else:
current_status = new_error current_status = new_error
# refresh plot (using parent class) if error is new and not on program-startup
if self.search_previous_errors(key, n_errors=1):
self.parent.write_html_figure(self.nwst_id)
self._update_status(key, current_status, detailed_message, last_occurrence) self._update_status(key, current_status, detailed_message, last_occurrence)
@ -507,7 +523,7 @@ class StationQC(object):
if self.search_previous_errors(key): if self.search_previous_errors(key):
self.send_mail(key, detailed_message) self.send_mail(key, detailed_message)
def search_previous_errors(self, key): def search_previous_errors(self, key, n_errors=None):
""" """
Check n_track + 1 previous statuses for errors. Check n_track + 1 previous statuses for errors.
If first item in list is no error but all others are return True (first time n_track errors appeared -- If first item in list is no error but all others are return True (first time n_track errors appeared --
@ -515,9 +531,12 @@ class StationQC(object):
In all other cases return True. In all other cases return True.
This also prevents sending status (e.g. mail) in case of program startup This also prevents sending status (e.g. mail) in case of program startup
""" """
if n_errors is not None:
n_errors = self.parameters.get('n_track') + 1
previous_errors = self.status_track.get(key) previous_errors = self.status_track.get(key)
# only if error list is filled n_track times # only if error list is filled n_track times
if previous_errors and len(previous_errors) == self.parameters.get('n_track') + 1: if previous_errors and len(previous_errors) == n_errors:
# if first entry was no error but all others are, return True (-> new Fail n_track times) # if first entry was no error but all others are, return True (-> new Fail n_track times)
if not previous_errors[0] and all(previous_errors[1:]): if not previous_errors[0] and all(previous_errors[1:]):
return True return True
@ -547,7 +566,7 @@ class StationQC(object):
dt = timedelta(seconds=n_track * interval) dt = timedelta(seconds=n_track * interval)
text = f'{key} FAIL status longer than {dt}: ' + message text = f'{key} FAIL status longer than {dt}: ' + message
msg = MIMEText(text) msg = MIMEText(text)
msg['Subject'] = f'new FAIL status on station {self.network}.{self.station}' msg['Subject'] = f'new FAIL status on station {self.nwst_id}'
msg['From'] = sender msg['From'] = sender
msg['To'] = ', '.join(addresses) msg['To'] = ', '.join(addresses)
@ -556,7 +575,6 @@ class StationQC(object):
s.sendmail(sender, addresses, msg.as_string()) s.sendmail(sender, addresses, msg.as_string())
s.quit() s.quit()
def status_other(self, detailed_message, status_message, last_occurrence=None, count=1): def status_other(self, detailed_message, status_message, last_occurrence=None, count=1):
key = 'other' key = 'other'
new_status = StatusOther(count=count, messages=[status_message]) new_status = StatusOther(count=count, messages=[status_message])
@ -611,7 +629,7 @@ class StationQC(object):
self.pb_rout_charge_analysis() self.pb_rout_charge_analysis()
def return_print_analysis(self): def return_print_analysis(self):
items = [f'{self.network}.{self.station}'] items = [self.nwst_id]
for key in self.keys: for key in self.keys:
status = self.status_dict[key] status = self.status_dict[key]
message = status.message message = status.message
@ -670,11 +688,10 @@ class StationQC(object):
self.warn(key, detailed_message=detailed_message, count=n_overvolt, self.warn(key, detailed_message=detailed_message, count=n_overvolt,
last_occurrence=self.get_last_occurrence(trace, overvolt)) last_occurrence=self.get_last_occurrence(trace, overvolt))
if len(undervolt) > 0: if len(undervolt) > 0:
# try calculate number of voltage peaks from gaps between indices # try calculate number of voltage peaks from gaps between indices
n_undervolt = len(np.where(np.diff(undervolt) > 1)[0]) + 1 n_undervolt = len(np.where(np.diff(undervolt) > 1)[0]) + 1
detailed_message = warn_message + f' {n_undervolt}x Voltage under {low_volt}V '\ detailed_message = warn_message + f' {n_undervolt}x Voltage under {low_volt}V ' \
+ self.get_last_occurrence_timestring(trace, undervolt) + self.get_last_occurrence_timestring(trace, undervolt)
self.warn(key, detailed_message=detailed_message, count=n_undervolt, self.warn(key, detailed_message=detailed_message, count=n_undervolt,
last_occurrence=self.get_last_occurrence(trace, undervolt)) last_occurrence=self.get_last_occurrence(trace, undervolt))
@ -693,7 +710,7 @@ class StationQC(object):
nsamp_av = int(trace.stats.sampling_rate) * timespan nsamp_av = int(trace.stats.sampling_rate) * timespan
av_temp_str = str(round(np.mean(temp[-nsamp_av:]), 1)) + deg_str av_temp_str = str(round(np.mean(temp[-nsamp_av:]), 1)) + deg_str
# dt of average # dt of average
dt_t_str = str(timedelta(seconds=int(timespan))) dt_t_str = str(timedelta(seconds=int(timespan))).replace(', 0:00:00', '')
# current temp # current temp
cur_temp = round(temp[-1], 1) cur_temp = round(temp[-1], 1)
if self.verbosity > 1: if self.verbosity > 1:
@ -778,7 +795,7 @@ class StationQC(object):
n_occurrences = len(np.where(np.diff(ind_array) > 1)[0]) + 1 n_occurrences = len(np.where(np.diff(ind_array) > 1)[0]) + 1
self.warn(key=key, self.warn(key=key,
detailed_message=f'Trace {trace.get_id()}: ' detailed_message=f'Trace {trace.get_id()}: '
f'Found {n_occurrences} occurrence(s) of {volt_lvl}V: {key}: {message}' f'Found {n_occurrences} occurrence(s) of {volt_lvl}V: {key}: {message}'
+ self.get_last_occurrence_timestring(trace, ind_array), + self.get_last_occurrence_timestring(trace, ind_array),
count=n_occurrences, count=n_occurrences,
last_occurrence=self.get_last_occurrence(trace, ind_array)) last_occurrence=self.get_last_occurrence(trace, ind_array))
@ -839,9 +856,9 @@ class StationQC(object):
n_occurrences = len(np.where(np.diff(under) > 1)[0]) + 1 n_occurrences = len(np.where(np.diff(under) > 1)[0]) + 1
voltage_dict[-1] = under voltage_dict[-1] = under
self.status_other(detailed_message=f'Trace {trace.get_id()}: ' self.status_other(detailed_message=f'Trace {trace.get_id()}: '
f'Voltage below {pb_ok}V in {len(under)} samples, {n_occurrences} time(s). ' f'Voltage below {pb_ok}V in {len(under)} samples, {n_occurrences} time(s). '
f'Mean voltage: {np.mean(voltage):.2}' f'Mean voltage: {np.mean(voltage):.2}'
+ self.get_last_occurrence_timestring(trace, under), + self.get_last_occurrence_timestring(trace, under),
status_message='under 1V ({})'.format(n_occurrences)) status_message='under 1V ({})'.format(n_occurrences))
# classify last voltage values # classify last voltage values
@ -860,8 +877,8 @@ class StationQC(object):
max_uncl = self.parameters.get('THRESHOLDS').get('unclassified') max_uncl = self.parameters.get('THRESHOLDS').get('unclassified')
if max_uncl and n_unclassified > max_uncl: if max_uncl and n_unclassified > max_uncl:
self.status_other(detailed_message=f'Trace {trace.get_id()}: ' self.status_other(detailed_message=f'Trace {trace.get_id()}: '
f'{n_unclassified}/{len(all_indices)} ' f'{n_unclassified}/{len(all_indices)} '
f'unclassified voltage values in channel {trace.get_id()}', f'unclassified voltage values in channel {trace.get_id()}',
status_message=f'{channel}: {n_unclassified} uncl.') status_message=f'{channel}: {n_unclassified} uncl.')
return False, voltage_dict, last_val return False, voltage_dict, last_val

View File

@ -22,7 +22,6 @@ except ImportError:
except ImportError: except ImportError:
raise ImportError('Could import neither of PySide2, PySide6 or PyQt5') raise ImportError('Could import neither of PySide2, PySide6 or PyQt5')
import matplotlib
from matplotlib.figure import Figure from matplotlib.figure import Figure
if QtGui.__package__ in ['PySide2', 'PyQt5', 'PySide6']: if QtGui.__package__ in ['PySide2', 'PyQt5', 'PySide6']:
@ -35,7 +34,7 @@ from obspy import UTCDateTime
from survBot import SurveillanceBot from survBot import SurveillanceBot
from write_utils import * from write_utils import *
from utils import get_bg_color from utils import get_bg_color, modify_stream_for_plot
try: try:
from rest_api.utils import get_station_iccid from rest_api.utils import get_station_iccid
@ -315,6 +314,7 @@ class MainWindow(QtWidgets.QMainWindow):
if st: if st:
self.plot_widget = PlotWidget(self) self.plot_widget = PlotWidget(self)
self.plot_widget.setWindowTitle(nwst_id) self.plot_widget.setWindowTitle(nwst_id)
st = modify_stream_for_plot(st, parameters=self.parameters)
st.plot(equal_scale=False, method='full', block=False, fig=self.plot_widget.canvas.fig) st.plot(equal_scale=False, method='full', block=False, fig=self.plot_widget.canvas.fig)
self.plot_widget.show() self.plot_widget.show()

View File

@ -3,6 +3,7 @@
import matplotlib import matplotlib
def get_bg_color(check_key, status, dt_thresh=None, hex=False): def get_bg_color(check_key, status, dt_thresh=None, hex=False):
message = status.message message = status.message
if check_key == 'last active': if check_key == 'last active':
@ -23,6 +24,7 @@ def get_bg_color(check_key, status, dt_thresh=None, hex=False):
bg_color = '#{:02x}{:02x}{:02x}'.format(*bg_color[:3]) bg_color = '#{:02x}{:02x}{:02x}'.format(*bg_color[:3])
return bg_color return bg_color
def get_color(key): def get_color(key):
# some GUI default colors # some GUI default colors
colors_dict = {'FAIL': (255, 50, 0, 255), colors_dict = {'FAIL': (255, 50, 0, 255),
@ -33,6 +35,7 @@ def get_color(key):
'undefined': (230, 230, 230, 255)} 'undefined': (230, 230, 230, 255)}
return colors_dict.get(key) return colors_dict.get(key)
def get_time_delay_color(dt, dt_thresh): def get_time_delay_color(dt, dt_thresh):
""" Set color of time delay after thresholds specified in self.dt_thresh """ """ Set color of time delay after thresholds specified in self.dt_thresh """
if dt < dt_thresh[0]: if dt < dt_thresh[0]:
@ -41,6 +44,7 @@ def get_time_delay_color(dt, dt_thresh):
return get_color('WARN') return get_color('WARN')
return get_color('FAIL') return get_color('FAIL')
def get_temp_color(temp, vmin=-10, vmax=60, cmap='coolwarm'): def get_temp_color(temp, vmin=-10, vmax=60, cmap='coolwarm'):
""" Get an rgba temperature value back from specified cmap, linearly interpolated between vmin and vmax. """ """ Get an rgba temperature value back from specified cmap, linearly interpolated between vmin and vmax. """
if type(temp) in [str]: if type(temp) in [str]:
@ -50,3 +54,51 @@ def get_temp_color(temp, vmin=-10, vmax=60, cmap='coolwarm'):
rgba = [int(255 * c) for c in cmap(val)] rgba = [int(255 * c) for c in cmap(val)]
return rgba return rgba
def modify_stream_for_plot(st, parameters):
""" copy (if necessary) and modify stream for plotting """
ch_units = parameters.get('CHANNEL_UNITS')
ch_transf = parameters.get('CHANNEL_TRANSFORM')
# if either of both are defined make copy
if ch_units or ch_transf:
st = st.copy()
# modify trace for plotting by multiplying unit factor (e.g. 1e-3 mV to V)
if ch_units:
for tr in st:
channel = tr.stats.channel
unit_factor = ch_units.get(channel)
if unit_factor:
tr.data = tr.data * float(unit_factor)
# modify trace for plotting by other arithmetic expressions
if ch_transf:
for tr in st:
channel = tr.stats.channel
transf = ch_transf.get(channel)
if transf:
tr.data = transform_trace(tr.data, transf)
return st
def transform_trace(data, transf):
"""
Transform trace with arithmetic operations in order, specified in transf
@param data: numpy array
@param transf: list of lists with arithmetic operations (e.g. [['*', '20'], ] -> multiply data by 20
"""
# This looks a little bit hardcoded, however it is safer than using e.g. "eval"
for operator_str, val in transf:
if operator_str == '+':
data = data + val
elif operator_str == '-':
data = data - val
elif operator_str == '*':
data = data * val
elif operator_str == '/':
data = data / val
else:
raise IOError(f'Unknown arithmethic operator string: {operator_str}')
return data

View File

@ -53,6 +53,6 @@ def write_html_row(fobj, items, html_key='td'):
def get_print_title_str(parameters): def get_print_title_str(parameters):
timespan = parameters.get('timespan') * 24 * 3600 timespan = parameters.get('timespan') * 24 * 3600
tdelta_str = str(timedelta(seconds=int(timespan))) tdelta_str = str(timedelta(seconds=int(timespan))).replace(', 0:00:00', '')
return f'Analysis table of router quality within the last {tdelta_str}' return f'Analysis table of router quality within the last {tdelta_str}'