24 Commits

Author SHA1 Message Date
47c3fbabf0 Merge pull request 'release/1.0' (#3) from release/1.0 into main
Reviewed-on: #3
2022-12-20 12:05:45 +01:00
d397ce377e [update] add network/station blacklists for mail functionality 2022-12-20 10:23:25 +01:00
d764c5c256 Merge remote-tracking branch 'origin/develop' into develop 2022-12-08 13:23:45 +01:00
a56781dca3 [minor] renamed and added stylesheets to git repository 2022-12-08 13:23:21 +01:00
fc64239c88 don't analysis values of data gaps when cheching for thresholds.
use stream.merge(fill_value=np.nan)
2022-12-06 16:34:08 +01:00
f0ae7da2be [update] add min_sample parameter, which controls ne number of samples that have to be of a specific voltage state before counting them as fail-state 2022-12-06 15:31:09 +01:00
a30cd8c0d4 [update] add trace axis ticking/min-max 2022-12-02 11:15:37 +01:00
a3378874fa [minor] parameter change 2022-12-02 11:15:01 +01:00
d21fb0ca3b [minor] parameter change 2022-11-29 10:42:29 +01:00
19b8df8f7d [minor] add html class for mobile (WIP) 2022-11-29 10:42:15 +01:00
6fc1e073c0 [bugfix] compared timedelta with int 2022-11-24 10:13:21 +01:00
56351ee700 [update] send message (mail) on station timeout 2022-11-23 11:52:26 +01:00
f45c5b20c5 [bugfix] set error state to active until mail is sent 2022-11-23 11:29:05 +01:00
7a2b7add04 [minor] parameter changes 2022-11-23 11:28:36 +01:00
ae0c2ef4e9 [minor] track activity status, modify html output for stylesheet 2022-11-22 18:06:25 +01:00
d35c176aab [update] add channel naming for plots 2022-11-22 15:51:15 +01:00
9444405453 [minor] reformat yaml file 2022-11-22 13:34:58 +01:00
a6d59c8c71 [update] added possibility to modify voltage level in plots from settings in parameters.yaml 2022-11-22 12:07:16 +01:00
3fe5fc48d1 [minor] re-ordered parameters 2022-11-21 10:36:46 +01:00
7da3db260a [update] error tracking + send email functionality 2022-11-17 09:52:04 +01:00
2c1e923920 [minor] add stylesheet to html header 2022-11-16 11:24:48 +01:00
8e42ac11c7 [update] complete rework of status handling (added Warn/Error classes etc.) 2022-11-15 17:19:39 +01:00
4d4324a1e9 [refactor] st_id -> nwst_id 2022-11-15 13:48:56 +01:00
4ba9c20d0f [update] add possibility to add columns with links to other web pages (e.g. seedlink monitor) 2022-11-15 13:44:19 +01:00
8 changed files with 783 additions and 197 deletions

View File

@@ -32,6 +32,8 @@ The main program with html output is executed by entering
python survBot.py -html path_for_html_output
```
There are example stylesheets in the folder *stylesheets* that can be copied into the path_for_html_output if desired.
The GUI can be loaded via
```shell script

View File

@@ -1,24 +1,27 @@
# Parameters file for Surveillance Bot
datapath: '/data/SDS/' # SC3 Datapath
networks: ['1Y', 'HA']
stations: '*'
locations: '*'
channels: ['EX1', 'EX2', 'EX3', 'VEI'] # Specify SOH channels, currently supported EX[1-3] and VEI
stations_blacklist: ['TEST', 'EREA']
networks_blacklist: []
datapath: "/data/SDS/" # SC3 Datapath
networks: ["1Y", "HA"] # select networks, list or str
stations: "*" # select stations, list or str
locations: "*" # select locations, list or str
channels: ["EX1", "EX2", "EX3", "VEI"] # Specify SOH channels, currently supported EX[1-3] and VEI
stations_blacklist: ["TEST", "EREA"] # exclude these stations
networks_blacklist: [] # exclude these networks
interval: 60 # Perform checks every x seconds
timespan: 3 # Check data of the recent x days
verbosity: 0
reread_parameters: True # reread parameters file (change parameters on runtime, not for itself/GUI refresh/datapath)
n_track: 300 # wait n_track * intervals before performing an action (i.e. send mail/end highlight status)
timespan: 7 # Check data of the recent x days
verbosity: 0 # verbosity flag
track_changes: True # tracks all changes since GUI startup by text highlighting (GUI only)
warn_count: False # show number of warnings and errors in table
min_sample: 3 # minimum samples for raising Warn/FAIL
dt_thresh: [300, 1800] # threshold (s) for timing delay colourisation (yellow/red)
html_figures: True # Create html figure directory and links
reread_parameters: True # reread parameters file (change parameters on runtime, not for itself/GUI refresh/datapath)
POWBOX:
pb_ok: 1 # Voltage for PowBox OK
pb_SOH2: # PowBox channel 2 voltage translations
-1: {"230V": "PBox under 1V", "12V": "PBox under 1V"}
1: {"230V": 'OK', "12V": "OK"}
1: {"230V": "OK", "12V": "OK"}
2: {"230V": "OFF", "12V": "OK"}
3: {"230V": "OK", "12V": "overvoltage"}
4: {"230V": "OK", "12V": "undervoltage"}
@@ -33,10 +36,49 @@ POWBOX:
4: {"router": "FAIL", "charger": "0 < resets < 3"}
5: {"router": "FAIL", "charger": "locked"}
# Thresholds for program warnings/voltage classifications
THRESHOLDS:
pb_thresh: 0.2 # Threshold for PowBox Voltage check +/- (V)
max_temp: 50 # max temperature for temperature warning
low_volt: 12 # min voltage for low voltage warning
high_volt: 14.8 # max voltage for over voltage warning
unclassified: 5 # min voltage samples not classified for warning
# ---------------------------------------- OPTIONAL PARAMETERS ---------------------------------------------------------
# add links to html table with specified key as column and value as relative link, interpretable string parameters:
# nw (e.g. 1Y), st (e.g. GR01A), nwst_id (e.g. 1Y.GR01A)
add_links:
# for example: slmon: {"URL": "path/{nw}_{st}.html", "text": "link"}
slmon: {"URL": "../slmon/{nw}_{st}.html", "text": "show"}
24h-plot: {"URL": "../scheli/{nw}/{st}.png", "text": "plot"}
# E-mail notifications
EMAIL:
mailserver: "localhost"
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
stations_blacklist: ['GR33'] # do not send emails for specific stations
networks_blacklist: [] # do not send emails for specific network
# names for plotting of the above defined parameter "channels" in the same order
channel_names: ["Temperature (°C)", "230V/12V Status (V)", "Router/Charger State (V)", "Logger Voltage (V)"]
# specify y-ticks (and ylims) giving, (ymin, ymax, step) for each of the above channels (0: default)
CHANNEL_TICKS:
- [-10, 50, 10]
- [1, 5, 1]
- [1, 5, 1]
- [9, 15, 1]
# 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]

39
stylesheets/desktop.css Normal file
View File

@@ -0,0 +1,39 @@
body {
background-color: #ffffff;
place-items: center;
text-align: center;
}
td {
border-radius: 4px;
padding: 0px;
}
th {
background-color: #999;
border-radius: 4px;
padding: 3px 1px;
}
a:link, a:visited {
background-color: #ccc;
color: #000;
text-decoration: none;
display: block;
border-radius: 4px;
border: 1px solid #bbb;
}
a:hover {
background-color: #aaa;
display: block;
}
.blink-bg {
animation: blinkingBackground 2s infinite;
}
@keyframes blinkingBackground{
0% { background-color: #ffcc00;}
50% { background-color: #ff3200;}
100% { background-color: #ffcc00;}
}

43
stylesheets/mobile.css Normal file
View File

@@ -0,0 +1,43 @@
body {
background-color: #ffffff;
place-items: center;
text-align: center;
}
td {
border-radius: 4px;
padding: 10px 2px;
}
th {
background-color: #999;
border-radius: 4px;
padding: 10px, 2px;
}
a:link {
background-color: #ccc;
color: #000;
text-decoration: none;
display: block;
border-radius: 4px;
border: 1px solid #bbb;
}
a:hover {
background-color: #aaa;
display: block;
}
.hidden-mobile {
display: none;
}
.blink-bg {
animation: blinkingBackground 2s infinite;
}
@keyframes blinkingBackground{
0% { background-color: #ffee00;}
50% { background-color: #ff3200;}
100% { background-color: #ffee00;}
}

View File

@@ -17,9 +17,18 @@ import matplotlib.pyplot as plt
from obspy import read, UTCDateTime, Stream
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
from utils import get_bg_color
from utils import get_bg_color, modify_stream_for_plot, trace_ylabels, trace_yticks
try:
import smtplib
from email.mime.text import MIMEText
mail_functionality = True
except ImportError:
print('Could not import smtplib or mail. Disabled sending mails.')
mail_functionality = False
pjoin = os.path.join
UP = "\x1B[{length}A"
@@ -32,12 +41,12 @@ def read_yaml(file_path):
return yaml.safe_load(f)
def nsl_from_id(st_id):
network, station, location = st_id.split('.')
def nsl_from_id(nwst_id):
network, station, location = nwst_id.split('.')
return dict(network=network, station=station, location=location)
def get_st_id(trace):
def get_nwst_id(trace):
stats = trace.stats
return f'{stats.network}.{stats.station}.' # {stats.location}'
@@ -64,6 +73,7 @@ class SurveillanceBot(object):
self.station_list = []
self.analysis_print_list = []
self.analysis_results = {}
self.status_track = {}
self.dataStream = Stream()
self.data = {}
self.print_count = 0
@@ -82,6 +92,8 @@ class SurveillanceBot(object):
self.networks_blacklist = self.parameters.get('networks_blacklist')
self.refresh_period = self.parameters.get('interval')
self.transform_parameters()
add_links = self.parameters.get('add_links')
self.add_links = add_links if add_links else {}
def transform_parameters(self):
for key in ['networks', 'stations', 'locations', 'channels']:
@@ -103,8 +115,8 @@ class SurveillanceBot(object):
if self.networks_blacklist and nw in self.networks_blacklist:
continue
if (networks == ['*'] or nw in networks) and (stations == ['*'] or st in stations):
st_id = f'{nw}.{st}.'
self.station_list.append(st_id)
nwst_id = f'{nw}.{st}.'
self.station_list.append(nwst_id)
def get_filenames(self):
self.filenames = []
@@ -145,7 +157,7 @@ class SurveillanceBot(object):
if filename in self.filenames_read:
continue
try:
st_new = read(filename)
st_new = read(filename, dtype=float)
# add file to read filenames to prevent re-reading in case it is not the current day (or end of
# previous day)
if not filename.endswith(f'{current_day:03}') and not (
@@ -155,14 +167,14 @@ class SurveillanceBot(object):
print(f'Could not read file {filename}:', e)
continue
self.dataStream += st_new
self.dataStream.merge()
self.dataStream.merge(fill_value=np.nan)
# organise data in dictionary with key for each station
for trace in self.dataStream:
st_id = get_st_id(trace)
if not st_id in self.data.keys():
self.data[st_id] = Stream()
self.data[st_id].append(trace)
nwst_id = get_nwst_id(trace)
if not nwst_id in self.data.keys():
self.data[nwst_id] = Stream()
self.data[nwst_id].append(trace)
def execute_qc(self):
if self.reread_parameters:
@@ -173,47 +185,65 @@ class SurveillanceBot(object):
self.analysis_print_list = []
self.analysis_results = {}
for st_id in sorted(self.station_list):
stream = self.data.get(st_id)
for nwst_id in sorted(self.station_list):
stream = self.data.get(nwst_id)
if stream:
nsl = nsl_from_id(st_id)
station_qc = StationQC(stream, nsl, self.parameters, self.keys, qc_starttime, self.verbosity,
print_func=self.print)
nsl = nsl_from_id(nwst_id)
station_qc = StationQC(self, stream, nsl, self.parameters, self.keys, qc_starttime,
self.verbosity, print_func=self.print,
status_track=self.status_track.get(nwst_id))
analysis_print_result = station_qc.return_print_analysis()
station_dict, warn_dict = station_qc.return_analysis()
station_dict = station_qc.return_analysis()
else:
analysis_print_result = self.get_no_data_station(st_id, to_print=True)
station_dict, warn_dict = self.get_no_data_station(st_id)
analysis_print_result = self.get_no_data_station(nwst_id, to_print=True)
station_dict = self.get_no_data_station(nwst_id)
self.analysis_print_list.append(analysis_print_result)
self.analysis_results[st_id] = (station_dict, warn_dict)
self.analysis_results[nwst_id] = station_dict
self.track_status()
self.update_status_message()
return 'ok'
def get_no_data_station(self, st_id, no_data='-', to_print=False):
delay = self.get_station_delay(st_id)
def track_status(self):
"""
tracks error status of the last n_track + 1 errors.
"""
n_track = self.parameters.get('n_track')
if not n_track or n_track < 1:
return
for nwst_id, analysis_dict in self.analysis_results.items():
if not nwst_id in self.status_track.keys():
self.status_track[nwst_id] = {}
for key, status in analysis_dict.items():
if not key in self.status_track[nwst_id].keys():
self.status_track[nwst_id][key] = []
track_lst = self.status_track[nwst_id][key]
# pop list until length is n_track + 1
while len(track_lst) > n_track:
track_lst.pop(0)
track_lst.append(status.is_error)
def get_no_data_station(self, nwst_id, no_data='-', to_print=False):
delay = self.get_station_delay(nwst_id)
if not to_print:
status_dict = {}
warn_dict = {}
for key in self.keys:
if key == 'last active':
status_dict[key] = timedelta(seconds=int(delay))
warn_dict[key] = 'No data within set timespan'
status_dict[key] = Status(message=timedelta(seconds=int(delay)), detailed_messages=['No data'])
else:
status_dict[key] = no_data
warn_dict[key] = 'No data'
return status_dict, warn_dict
status_dict[key] = Status(message=no_data, detailed_messages=['No data'])
return status_dict
else:
items = [st_id.rstrip('.')] + [fancy_timestr(timedelta(seconds=int(delay)))]
items = [nwst_id.rstrip('.')] + [fancy_timestr(timedelta(seconds=int(delay)))]
for _ in range(len(self.keys) - 1):
items.append(no_data)
return items
def get_station_delay(self, st_id):
def get_station_delay(self, nwst_id):
""" try to get station delay from SDS archive using client"""
locations = ['', '0', '00']
channels = ['HHZ', 'HHE', 'HHN', 'VEI', 'EX1', 'EX2', 'EX3']
network, station = st_id.split('.')[:2]
network, station = nwst_id.split('.')[:2]
times = []
for channel in channels:
@@ -248,7 +278,7 @@ class SurveillanceBot(object):
if self.outpath_html:
self.write_html_table()
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:
self.print_analysis()
time.sleep(self.refresh_period)
@@ -276,11 +306,11 @@ class SurveillanceBot(object):
self.plot_hour += 1
return True
def get_fig_path_abs(self, st_id):
return pjoin(self.outpath_html, self.get_fig_path_rel(st_id))
def get_fig_path_abs(self, nwst_id):
return pjoin(self.outpath_html, self.get_fig_path_rel(nwst_id))
def get_fig_path_rel(self, st_id, fig_format='png'):
return os.path.join(self.html_fig_dir, f'{st_id.rstrip(".")}.{fig_format}')
def get_fig_path_rel(self, nwst_id, fig_format='png'):
return os.path.join(self.html_fig_dir, f'{nwst_id.rstrip(".")}.{fig_format}')
def check_fig_dir(self):
fdir = pjoin(self.outpath_html, self.html_fig_dir)
@@ -292,23 +322,50 @@ class SurveillanceBot(object):
os.mkdir(self.outpath_html)
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():
return
for nwst_id in self.station_list:
self.write_html_figure(nwst_id)
def write_html_figure(self, nwst_id):
""" Write figure for html for specified station """
self.check_fig_dir()
for st_id in self.station_list:
fig = plt.figure(figsize=(16, 9))
fnout = self.get_fig_path_abs(st_id)
st = self.data.get(st_id)
fnout = self.get_fig_path_abs(nwst_id)
st = self.data.get(nwst_id)
if st:
# TODO: this section might fail, adding try-except block for analysis and to prevent program from crashing
try:
st = modify_stream_for_plot(st, parameters=self.parameters)
st.plot(fig=fig, show=False, draw=False, block=False, equal_scale=False, method='full')
trace_ylabels(fig, self.parameters, self.verbosity)
trace_yticks(fig, self.parameters, self.verbosity)
except Exception as e:
print(f'Could not generate plot for {nwst_id}:')
print(traceback.format_exc())
if len(fig.axes) > 0:
ax = fig.axes[0]
ax.set_title(f'Hourly refreshed plot at (UTC) {UTCDateTime.now().strftime("%Y-%m-%d %H:%M:%S")}')
ax.set_title(f'Plot refreshed at (UTC) {UTCDateTime.now().strftime("%Y-%m-%d %H:%M:%S")}. '
f'Refreshed hourly or on FAIL status.')
for ax in fig.axes:
ax.grid(True, alpha=0.1)
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', default_header_color='#999', hide_keys_mobile=('other')):
def get_html_class(status=None, check_key=None):
""" helper function for html class if a certain condition is fulfilled """
html_class = None
if status and status.is_active:
html_class = 'blink-bg'
if check_key in hide_keys_mobile:
html_class = 'hidden-mobile'
return html_class
self.check_html_dir()
fnout = pjoin(self.outpath_html, 'survBot_out.html')
if not fnout:
@@ -316,23 +373,32 @@ class SurveillanceBot(object):
try:
with open(fnout, 'w') as outfile:
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)
# First write header items
header_items = [dict(text='Station', color=default_color)]
for check_key in self.keys:
item = dict(text=check_key, color=default_color)
header = self.keys.copy()
# add columns for additional links
for key in self.add_links:
header.insert(-1, key)
header_items = [dict(text='Station', color=default_header_color)]
for check_key in header:
html_class = get_html_class(check_key=check_key)
item = dict(text=check_key, color=default_header_color, html_class=html_class)
header_items.append(item)
write_html_row(outfile, header_items, html_key='th')
# Write all cells
for st_id in self.station_list:
fig_name = self.get_fig_path_rel(st_id)
col_items = [dict(text=st_id.rstrip('.'), color=default_color, image_src=fig_name)]
for check_key in self.keys:
status_dict, detailed_dict = self.analysis_results.get(st_id)
for nwst_id in self.station_list:
fig_name = self.get_fig_path_rel(nwst_id)
nwst_id_str = nwst_id.rstrip('.')
col_items = [dict(text=nwst_id_str, color=default_color, hyperlink=fig_name,
bold=True, tooltip=f'Show plot of {nwst_id_str}')]
for check_key in header:
if check_key in self.keys:
status_dict = self.analysis_results.get(nwst_id)
status = status_dict.get(check_key)
message, detailed_message = status.get_status_str()
# get background color
dt_thresh = [timedelta(seconds=sec) for sec in self.dt_thresh]
@@ -342,12 +408,23 @@ class SurveillanceBot(object):
# add degree sign for temp
if check_key == 'temp':
if not type(status) in [str]:
status = str(status) + deg_str
if not type(message) in [str]:
message = str(message) + deg_str
item = dict(text=str(status), tooltip=str(detailed_dict.get(check_key)),
color=bg_color)
html_class = get_html_class(status=status, check_key=check_key)
item = dict(text=str(message), tooltip=str(detailed_message), color=bg_color,
html_class=html_class)
elif check_key in self.add_links:
value = self.add_links.get(check_key).get('URL')
link_text = self.add_links.get(check_key).get('text')
if not value:
continue
nw, st = nwst_id.split('.')[:2]
hyperlink_dict = dict(nw=nw, st=st, nwst_id=nwst_id)
link = value.format(**hyperlink_dict)
item = dict(text=link_text, tooltip=link, hyperlink=link, color=default_color)
col_items.append(item)
write_html_row(outfile, col_items)
finish_html_table(outfile)
@@ -361,7 +438,7 @@ class SurveillanceBot(object):
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")} | ' \
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}'
def print(self, string, **kwargs):
@@ -378,12 +455,13 @@ class SurveillanceBot(object):
class StationQC(object):
def __init__(self, stream, nsl, parameters, keys, starttime, verbosity, print_func):
def __init__(self, parent, stream, nsl, parameters, keys, starttime, verbosity, print_func, status_track={}):
"""
Station Quality Check class.
:param nsl: dictionary containing network, station and location (key: str)
:param parameters: parameters dictionary from parameters.yaml file
"""
self.parent = parent
self.stream = stream
self.nsl = nsl
self.network = nsl.get('network')
@@ -395,51 +473,186 @@ class StationQC(object):
self.last_active = False
self.print = print_func
timespan = self.parameters.get('timespan') * 24 * 3600
self.analysis_starttime = self.program_starttime - timespan
self.keys = keys
self.detailed_status_dict = {key: None for key in self.keys}
self.status_dict = {key: '-' for key in self.keys}
self.activity_check()
self.status_dict = {key: Status() for key in self.keys}
self.analyse_channels()
if not status_track:
status_track = {}
self.status_track = status_track
def status_ok(self, key, message=None, status_message='OK'):
self.status_dict[key] = status_message
self.detailed_status_dict[key] = message
self.start()
def warn(self, key, detailed_message, status_message='WARN'):
# update detailed status if already existing
current_message = self.detailed_status_dict.get(key)
current_message = '' if current_message in [None, '-'] else current_message + ' | '
self.detailed_status_dict[key] = current_message + detailed_message
@property
def nwst_id(self):
return f'{self.network}.{self.station}'
# this is becoming a little bit too complicated (adding warnings to existing)
current_status_message = self.status_dict.get(key)
current_status_message = '' if current_status_message in [None, 'OK', '-'] else current_status_message + ' | '
self.status_dict[key] = current_status_message + status_message
def status_ok(self, key, detailed_message="Everything OK", status_message='OK', overwrite=False):
current_status = self.status_dict.get(key)
# do not overwrite existing warnings or errors
if not overwrite and (current_status.is_warn or current_status.is_error):
return
self.status_dict[key] = StatusOK(message=status_message, detailed_messages=[detailed_message])
def warn(self, key, detailed_message, last_occurrence=None, count=1):
if key == 'other':
self.status_other(detailed_message, last_occurrence, count)
new_warn = StatusWarn(count=count, show_count=self.parameters.get('warn_count'))
current_status = self.status_dict.get(key)
# change this to something more useful, SMS/EMAIL/PUSH
if self.verbosity:
self.print(f'{UTCDateTime()}: {detailed_message}', flush=False)
# if error, do not overwrite with warning
if current_status.is_error:
return
if current_status.is_warn:
current_status.count += count
else:
current_status = new_warn
self._update_status(key, current_status, detailed_message, last_occurrence)
# warnings.warn(message)
def error(self, key, message):
self.detailed_status_dict[key] = message
self.status_dict[key] = 'FAIL'
# change this to something more useful, SMS/EMAIL/PUSH
# # update detailed status if already existing
# current_message = self.detailed_status_dict.get(key)
# current_message = '' if current_message in [None, '-'] else current_message + ' | '
# self.detailed_status_dict[key] = current_message + detailed_message
#
# # this is becoming a little bit too complicated (adding warnings to existing)
# current_status_message = self.status_dict.get(key)
# current_status_message = '' if current_status_message in [None, 'OK', '-'] else current_status_message + ' | '
# self.status_dict[key] = current_status_message + status_message
def error(self, key, detailed_message, last_occurrence=None, count=1):
new_error = StatusError(count=count, show_count=self.parameters.get('warn_count'))
current_status = self.status_dict.get(key)
if current_status.is_error:
current_status.count += count
else:
current_status = new_error
# if error is new and not on program-startup set active and refresh plot (using parent class)
if self.search_previous_errors(key, n_errors=1) is True:
self.parent.write_html_figure(self.nwst_id)
if self.verbosity:
self.print(f'{UTCDateTime()}: {message}', flush=False)
# warnings.warn(message)
self.print(f'{UTCDateTime()}: {detailed_message}', flush=False)
def activity_check(self):
# do not send error mail if this is the first run (e.g. program startup) or state was already error (unchanged)
if self.search_previous_errors(key) is True:
self.send_mail(key, status_type='FAIL', additional_message=detailed_message)
# set status to "inactive" after sending info mail
current_status.is_active = False
elif self.search_previous_errors(key) == 'active':
current_status.is_active = True
self._update_status(key, current_status, detailed_message, last_occurrence)
def search_previous_errors(self, key, n_errors=None):
"""
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 ALL n_track + 1 are error: error is old)
If last item is error but not all items are error yet return keyword 'active' -> error active, no message sent
In all other cases return False.
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)
# only if error list is filled n_track times
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 not previous_errors[0] and all(previous_errors[1:]):
return True
# in case previous_errors exists, last item is error but not all items are error, error still active
elif previous_errors and previous_errors[-1] and not all(previous_errors):
return 'active'
return False
def send_mail(self, key, status_type, additional_message=''):
""" Send info mail using parameters specified in parameters file """
if not mail_functionality:
if self.verbosity:
print('Mail functionality disabled. Return')
return
mail_params = self.parameters.get('EMAIL')
if not mail_params:
if self.verbosity:
print('parameter "EMAIL" not set in parameter file. Return')
return
stations_blacklist = mail_params.get('stations_blacklist')
if stations_blacklist and self.station in stations_blacklist:
if self.verbosity:
print(f'Station {self.station} listed in blacklist. Return')
return
networks_blacklist = mail_params.get('networks_blacklist')
if networks_blacklist and self.network in networks_blacklist:
if self.verbosity:
print(f'Station {self.station} of network {self.network} listed in blacklist. Return')
return
sender = mail_params.get('sender')
addresses = mail_params.get('addresses')
server = mail_params.get('mailserver')
if not sender or not addresses:
if self.verbosity:
print('Mail sender or addresses not correctly defined. Return')
return
dt = self.get_dt_for_action()
text = f'{key}: Status {status_type} longer than {dt}: ' + additional_message
msg = MIMEText(text)
msg['Subject'] = f'new message on station {self.nwst_id}'
msg['From'] = sender
msg['To'] = ', '.join(addresses)
# send message via SMTP server
s = smtplib.SMTP(server)
s.sendmail(sender, addresses, msg.as_string())
s.quit()
def get_dt_for_action(self):
n_track = self.parameters.get('n_track')
interval = self.parameters.get('interval')
dt = timedelta(seconds=n_track * interval)
return dt
def status_other(self, detailed_message, status_message, last_occurrence=None, count=1):
key = 'other'
new_status = StatusOther(count=count, messages=[status_message])
current_status = self.status_dict.get(key)
if current_status.is_other:
current_status.count += count
current_status.messages.append(status_message)
else:
current_status = new_status
self._update_status(key, current_status, detailed_message, last_occurrence)
def _update_status(self, key, current_status, detailed_message, last_occurrence):
current_status.detailed_messages.append(detailed_message)
current_status.last_occurrence = last_occurrence
self.status_dict[key] = current_status
def activity_check(self, key='last_active'):
self.last_active = self.last_activity()
if not self.last_active:
message = 'FAIL'
status = StatusError()
else:
message = timedelta(seconds=int(self.program_starttime - self.last_active))
self.status_dict['last active'] = message
dt_active = timedelta(seconds=int(self.program_starttime - self.last_active))
status = Status(message=dt_active)
self.check_for_inactive_message(key, dt_active)
self.status_dict['last active'] = status
def last_activity(self):
if not self.stream:
@@ -450,39 +663,56 @@ class StationQC(object):
if len(endtimes) > 0:
return max(endtimes)
def check_for_inactive_message(self, key, dt_active):
dt_action = self.get_dt_for_action()
interval = self.parameters.get('interval')
if dt_action <= dt_active < dt_action + timedelta(seconds=interval):
self.send_mail(key, status_type='Inactive')
def start(self):
self.analyse_channels()
def analyse_channels(self):
timespan = self.parameters.get('timespan') * 24 * 3600
self.analysis_starttime = self.program_starttime - timespan
if self.verbosity > 0:
self.print(150 * '#')
self.print('This is StationQT. Calculating quality for station'
' {network}.{station}.{location}'.format(**self.nsl))
self.activity_check()
self.voltage_analysis()
self.pb_temp_analysis()
self.pb_power_analysis()
self.pb_rout_charge_analysis()
def return_print_analysis(self):
items = [f'{self.network}.{self.station}']
items = [self.nwst_id]
for key in self.keys:
item = self.status_dict[key]
status = self.status_dict[key]
message = status.message
if key == 'last active':
items.append(fancy_timestr(item))
items.append(fancy_timestr(message))
elif key == 'temp':
items.append(str(item) + deg_str)
items.append(str(message) + deg_str)
else:
items.append(str(item))
items.append(str(message))
return items
def return_analysis(self):
return self.status_dict, self.detailed_status_dict
return self.status_dict
def get_last_occurrence_timestring(self, trace, indices):
""" returns a nicely formatted string of the timedelta since program starttime and occurrence and abs time"""
last_occur = self.get_time(trace, indices[-1])
last_occur = self.get_last_occurrence(trace, indices)
if not last_occur:
return ''
last_occur_dt = timedelta(seconds=int(self.program_starttime - last_occur))
return f', Last occurrence: {last_occur_dt} ({last_occur.strftime("%Y-%m-%d %H:%M:%S")})'
def get_last_occurrence(self, trace, indices):
return self.get_time(trace, indices[-1])
def voltage_analysis(self, channel='VEI'):
""" Analyse voltage channel for over/undervoltage """
key = 'voltage'
@@ -501,24 +731,25 @@ class StationQC(object):
undervolt = np.where(voltage < low_volt)[0]
if len(overvolt) == 0 and len(undervolt) == 0:
self.status_ok(key, message=f'U={(voltage[-1])}V')
self.status_ok(key, detailed_message=f'U={(voltage[-1])}V')
return
n_overvolt = 0
n_undervolt = 0
warn_message = f'Trace {trace.get_id()}:'
if len(overvolt) > 0:
# try calculate number of voltage peaks from gaps between indices
n_overvolt = len(np.where(np.diff(overvolt) > 1)[0]) + 1
warn_message += f' {n_overvolt}x Voltage over {high_volt}V' \
detailed_message = warn_message + f' {n_overvolt}x Voltage over {high_volt}V' \
+ self.get_last_occurrence_timestring(trace, overvolt)
self.warn(key, detailed_message=detailed_message, count=n_overvolt,
last_occurrence=self.get_last_occurrence(trace, overvolt))
if len(undervolt) > 0:
# try calculate number of voltage peaks from gaps between indices
n_undervolt = len(np.where(np.diff(undervolt) > 1)[0]) + 1
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.warn(key, detailed_message=warn_message, status_message='WARN ({})'.format(n_overvolt + n_undervolt))
self.warn(key, detailed_message=detailed_message, count=n_undervolt,
last_occurrence=self.get_last_occurrence(trace, undervolt))
def pb_temp_analysis(self, channel='EX1'):
""" Analyse PowBox temperature output. """
@@ -534,7 +765,7 @@ class StationQC(object):
nsamp_av = int(trace.stats.sampling_rate) * timespan
av_temp_str = str(round(np.mean(temp[-nsamp_av:]), 1)) + deg_str
# 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
cur_temp = round(temp[-1], 1)
if self.verbosity > 1:
@@ -547,14 +778,14 @@ class StationQC(object):
t_check = np.where(temp > max_temp)[0]
if len(t_check) > 0:
self.warn(key=key,
status_message=cur_temp,
detailed_message=f'Trace {trace.get_id()}: '
f'Temperature over {max_temp}\N{DEGREE SIGN} at {trace.get_id()}!'
+ self.get_last_occurrence_timestring(trace, t_check))
+ self.get_last_occurrence_timestring(trace, t_check),
last_occurrence=self.get_last_occurrence(trace, t_check))
else:
self.status_ok(key,
status_message=cur_temp,
message=f'Average temperature of last {dt_t_str}: {av_temp_str}')
detailed_message=f'Average temperature of last {dt_t_str}: {av_temp_str}')
def pb_power_analysis(self, channel='EX2', pb_dict_key='pb_SOH2'):
""" Analyse EX2 channel of PowBox """
@@ -604,22 +835,53 @@ class StationQC(object):
def in_depth_voltage_check(self, trace, voltage_dict, soh_params, last_val):
""" Associate values in voltage_dict to error messages specified in SOH_params and warn."""
for volt_lvl, ind_array in voltage_dict.items():
if volt_lvl == 1: continue # No need to do anything here
if volt_lvl == 1:
continue # No need to do anything here
if len(ind_array) > 0:
# get result from parameter dictionary for voltage level
result = soh_params.get(volt_lvl)
for key, message in result.items():
# if result is OK, continue with next voltage level
if message == 'OK':
self.status_ok(key)
continue
# try calculate number of voltage peaks from gaps between indices
n_occurrences = len(np.where(np.diff(ind_array) > 1)[0]) + 1
if volt_lvl > 1:
n_occurrences = self.calc_occurrences(ind_array)
self.warn(key=key,
detailed_message=f'Trace {trace.get_id()}: '
f'Found {n_occurrences} occurrence(s) of {volt_lvl}V: {key}: {message}'
+ self.get_last_occurrence_timestring(trace, ind_array),
status_message='WARN ({})'.format(n_occurrences))
if last_val != 1:
self.error(key, message=f'Last PowBox voltage state {last_val}V: {message}')
count=n_occurrences,
last_occurrence=self.get_last_occurrence(trace, ind_array))
# if last_val == current voltage (which is not 1) -> FAIL or last_val < 1: PBox no data
if volt_lvl == last_val or (volt_lvl == -1 and last_val < 1):
self.error(key, detailed_message=f'Last PowBox voltage state {last_val}V: {message}')
def calc_occurrences(self, ind_array):
# try calculate number of voltage peaks/plateaus from gaps between indices
if len(ind_array) == 0:
return 0
else:
# start index at 1 if there are gaps (n_peaks = n_gaps + 1)
n_occurrences = 1
min_samples = self.parameters.get('min_sample')
if not min_samples:
min_samples = 1
# calculated differences in index array, diff > 1: gap, diff == 1: within peak/plateau
diffs = np.diff(ind_array)
gap_start_inds = np.where(np.diff(ind_array) > 1)[0]
# iterate over all gaps and check "min_samples" before the gap
for gsi in gap_start_inds:
# right boundary index of peak (gap index - 1)
peak_rb_ind = gsi - 1
# left boundary index of peak
peak_lb_ind = max([0, peak_rb_ind - min_samples])
if all(diffs[peak_lb_ind: peak_rb_ind] == 1):
n_occurrences += 1
return n_occurrences
def get_trace(self, stream, keys):
if not type(keys) == list:
@@ -673,8 +935,7 @@ class StationQC(object):
# try calculate number of occurences from gaps between indices
n_occurrences = len(np.where(np.diff(under) > 1)[0]) + 1
voltage_dict[-1] = under
self.warn(key='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'Mean voltage: {np.mean(voltage):.2}'
+ self.get_last_occurrence_timestring(trace, under),
@@ -695,7 +956,7 @@ class StationQC(object):
n_unclassified = len(unclassified_indices)
max_uncl = self.parameters.get('THRESHOLDS').get('unclassified')
if max_uncl and n_unclassified > max_uncl:
self.warn(key='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'unclassified voltage values in channel {trace.get_id()}',
status_message=f'{channel}: {n_unclassified} uncl.')
@@ -707,6 +968,96 @@ class StationQC(object):
return trace.stats.starttime + trace.stats.delta * index
class Status(object):
def __init__(self, message=None, detailed_messages=None, count: int = 0, last_occurrence=None, show_count=True):
if message is None:
message = '-'
if detailed_messages is None:
detailed_messages = []
self.show_count = show_count
self.message = message
self.messages = [message]
self.detailed_messages = detailed_messages
self.count = count
self.last_occurrence = last_occurrence
self.is_warn = None
self.is_error = None
self.is_other = False
self.is_active = False
def set_warn(self):
self.is_warn = True
def set_error(self):
self.is_warn = False
self.is_error = True
def set_ok(self):
self.is_warn = False
self.is_error = False
def get_status_str(self):
message = self.message
if self.count > 1 and self.show_count:
message += f' ({self.count})'
detailed_message = ''
for index, dm in enumerate(self.detailed_messages):
if index > 0:
detailed_message += ' | '
detailed_message += dm
return message, detailed_message
class StatusOK(Status):
def __init__(self, message='OK', detailed_messages=None):
super(StatusOK, self).__init__(message=message, detailed_messages=detailed_messages)
self.set_ok()
class StatusWarn(Status):
def __init__(self, message='WARN', count=1, last_occurence=None, detailed_messages=None, show_count=False):
super(StatusWarn, self).__init__(message=message, count=count, last_occurrence=last_occurence,
detailed_messages=detailed_messages, show_count=show_count)
self.set_warn()
class StatusError(Status):
def __init__(self, message='FAIL', count=1, last_occurence=None, detailed_messages=None, show_count=False):
super(StatusError, self).__init__(message=message, count=count, last_occurrence=last_occurence,
detailed_messages=detailed_messages, show_count=show_count)
self.set_error()
class StatusOther(Status):
def __init__(self, messages=None, count=1, last_occurence=None, detailed_messages=None):
super(StatusOther, self).__init__(count=count, last_occurrence=last_occurence,
detailed_messages=detailed_messages)
if messages is None:
messages = []
self.messages = messages
self.is_other = True
def get_status_str(self):
if self.messages == []:
return '-'
message = ''
for index, mes in enumerate(self.messages):
if index > 0:
message += ' | '
message += mes
detailed_message = ''
for index, dm in enumerate(self.detailed_messages):
if index > 0:
detailed_message += ' | '
detailed_message += dm
return message, detailed_message
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Call survBot')
parser.add_argument('-html', dest='html_path', default=None, help='filepath for HTML output')

View File

@@ -22,7 +22,6 @@ except ImportError:
except ImportError:
raise ImportError('Could import neither of PySide2, PySide6 or PyQt5')
import matplotlib
from matplotlib.figure import Figure
if QtGui.__package__ in ['PySide2', 'PyQt5', 'PySide6']:
@@ -35,7 +34,7 @@ from obspy import UTCDateTime
from survBot import SurveillanceBot
from write_utils import *
from utils import get_bg_color
from utils import get_bg_color, modify_stream_for_plot, trace_ylabels, trace_yticks
try:
from rest_api.utils import get_station_iccid
@@ -130,10 +129,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.table.setRowCount(len(station_list))
self.table.setHorizontalHeaderLabels(keys)
for index, st_id in enumerate(station_list):
for index, nwst_id in enumerate(station_list):
item = QtWidgets.QTableWidgetItem()
item.setText(str(st_id.rstrip('.')))
item.setData(QtCore.Qt.UserRole, st_id)
item.setText(str(nwst_id.rstrip('.')))
item.setData(QtCore.Qt.UserRole, nwst_id)
self.table.setVerticalHeaderItem(index, item)
self.main_layout.addWidget(self.table)
@@ -180,38 +179,38 @@ class MainWindow(QtWidgets.QMainWindow):
header_item = self.table.verticalHeaderItem(row_ind)
if not header_item:
return
st_id = header_item.data(QtCore.Qt.UserRole)
nwst_id = header_item.data(QtCore.Qt.UserRole)
context_menu = QtWidgets.QMenu()
read_sms = context_menu.addAction('Get last SMS')
send_sms = context_menu.addAction('Send SMS')
action = context_menu.exec_(self.mapToGlobal(self.last_mouse_loc))
if action == read_sms:
self.read_sms(st_id)
self.read_sms(nwst_id)
elif action == send_sms:
self.send_sms(st_id)
self.send_sms(nwst_id)
def read_sms(self, st_id):
def read_sms(self, nwst_id):
""" Read recent SMS over rest_api using whereversim portal """
station = st_id.split('.')[1]
station = nwst_id.split('.')[1]
iccid = get_station_iccid(station)
if not iccid:
print('Could not find iccid for station', st_id)
print('Could not find iccid for station', nwst_id)
return
sms_widget = ReadSMSWidget(parent=self, iccid=iccid)
sms_widget.setWindowTitle(f'Recent SMS of station: {st_id}')
sms_widget.setWindowTitle(f'Recent SMS of station: {nwst_id}')
if sms_widget.data:
sms_widget.show()
else:
self.notification('No recent messages found.')
def send_sms(self, st_id):
def send_sms(self, nwst_id):
""" Send SMS over rest_api using whereversim portal """
station = st_id.split('.')[1]
station = nwst_id.split('.')[1]
iccid = get_station_iccid(station)
sms_widget = SendSMSWidget(parent=self, iccid=iccid)
sms_widget.setWindowTitle(f'Send SMS to station: {st_id}')
sms_widget.setWindowTitle(f'Send SMS to station: {nwst_id}')
sms_widget.show()
def set_clear_on_refresh(self):
@@ -230,19 +229,19 @@ class MainWindow(QtWidgets.QMainWindow):
self.fill_status_bar()
for col_ind, check_key in enumerate(self.survBot.keys):
for row_ind, st_id in enumerate(self.survBot.station_list):
status_dict, detailed_dict = self.survBot.analysis_results.get(st_id)
for row_ind, nwst_id in enumerate(self.survBot.station_list):
status_dict = self.survBot.analysis_results.get(nwst_id)
status = status_dict.get(check_key)
detailed_message = detailed_dict.get(check_key)
message, detailed_message = status.get_status_str()
dt_thresh = [timedelta(seconds=sec) for sec in self.dt_thresh]
bg_color = get_bg_color(check_key, status, dt_thresh)
if check_key == 'temp':
if not type(status) in [str]:
status = str(status) + deg_str
if not type(message) in [str]:
message = str(message) + deg_str
# Continue if nothing changed
text = str(status)
text = str(message)
cur_item = self.table.item(row_ind, col_ind)
if cur_item and text == cur_item.text():
if not self.parameters.get('track_changes') or self.clear_on_refresh:
@@ -253,9 +252,9 @@ class MainWindow(QtWidgets.QMainWindow):
# Create new data item
item = QtWidgets.QTableWidgetItem()
item.setText(str(status))
item.setText(str(message))
item.setTextAlignment(QtCore.Qt.AlignCenter)
item.setData(QtCore.Qt.UserRole, (st_id, check_key))
item.setData(QtCore.Qt.UserRole, (nwst_id, check_key))
# if text changed (known from above) set highlight color/font else (new init) set to default
cur_item = self.table.item(row_ind, col_ind)
@@ -310,12 +309,15 @@ class MainWindow(QtWidgets.QMainWindow):
vheader.setSectionResizeMode(index, QtWidgets.QHeaderView.Stretch)
def plot_stream(self, item):
st_id, check = item.data(QtCore.Qt.UserRole)
st = self.survBot.data.get(st_id)
nwst_id, check = item.data(QtCore.Qt.UserRole)
st = self.survBot.data.get(nwst_id)
if st:
self.plot_widget = PlotWidget(self)
self.plot_widget.setWindowTitle(st_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)
trace_ylabels(fig=self.plot_widget.canvas.fig, parameters=self.parameters)
trace_yticks(fig=self.plot_widget.canvas.fig, parameters=self.parameters)
self.plot_widget.show()
def notification(self, text):

112
utils.py
View File

@@ -3,18 +3,20 @@
import matplotlib
def get_bg_color(check_key, status, dt_thresh=None, hex=False):
message = status.message
if check_key == 'last active':
bg_color = get_time_delay_color(status, dt_thresh)
bg_color = get_time_delay_color(message, dt_thresh)
elif check_key == 'temp':
bg_color = get_temp_color(status)
bg_color = get_temp_color(message)
else:
statussplit = status.split(' ')
if len(statussplit) > 1 and statussplit[0] == 'WARN':
x = int(status.split(' ')[-1].lstrip('(').rstrip(')'))
bg_color = get_color('WARNX')(x)
if status.is_warn:
bg_color = get_color('WARNX')(status.count)
elif status.is_error:
bg_color = get_color('FAIL')
else:
bg_color = get_color(status)
bg_color = get_color(message)
if not bg_color:
bg_color = get_color('undefined')
@@ -22,16 +24,18 @@ def get_bg_color(check_key, status, dt_thresh=None, hex=False):
bg_color = '#{:02x}{:02x}{:02x}'.format(*bg_color[:3])
return bg_color
def get_color(key):
# some GUI default colors
colors_dict = {'FAIL': (255, 50, 0, 255),
'NO DATA': (255, 255, 125, 255),
'WARN': (255, 255, 125, 255),
'WARNX': lambda x: (min([255, 200 + x ** 2]), 255, 125, 255),
'WARN': (255, 255, 80, 255),
'WARNX': lambda x: (min([255, 200 + x ** 2]), 255, 80, 255),
'OK': (125, 255, 125, 255),
'undefined': (230, 230, 230, 255)}
return colors_dict.get(key)
def get_time_delay_color(dt, dt_thresh):
""" Set color of time delay after thresholds specified in self.dt_thresh """
if dt < dt_thresh[0]:
@@ -40,6 +44,7 @@ def get_time_delay_color(dt, dt_thresh):
return get_color('WARN')
return get_color('FAIL')
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. """
if type(temp) in [str]:
@@ -49,3 +54,92 @@ def get_temp_color(temp, vmin=-10, vmax=60, cmap='coolwarm'):
rgba = [int(255 * c) for c in cmap(val)]
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
def trace_ylabels(fig, parameters, verbosity=0):
"""
Adds channel names to y-axis if defined in parameters.
Can get mixed up if channel order in stream and channel names defined in parameters.yaml differ, but it is
difficult to assess the correct order from Obspy plotting routing.
"""
names = parameters.get('channel_names')
if not names: # or not len(st.traces):
return
if not len(names) == len(fig.axes):
if verbosity:
print('Mismatch in axis and label lengths. Not adding plot labels')
return
for channel_name, ax in zip(names, fig.axes):
if channel_name:
ax.set_ylabel(channel_name)
def trace_yticks(fig, parameters, verbosity=0):
"""
Adds channel names to y-axis if defined in parameters.
Can get mixed up if channel order in stream and channel names defined in parameters.yaml differ, but it is
difficult to assess the correct order from Obspy plotting routing.
"""
ticks = parameters.get('CHANNEL_TICKS')
if not ticks:
return
if not len(ticks) == len(fig.axes):
if verbosity:
print('Mismatch in axis tick and label lengths. Not changing plot ticks.')
return
for ytick_tripple, ax in zip(ticks, fig.axes):
if not ytick_tripple:
continue
ymin, ymax, step = ytick_tripple
yticks = list(range(ymin, ymax + step, step))
ax.set_yticks(yticks)
ax.set_ylim(ymin - step, ymax + step)

View File

@@ -1,55 +1,68 @@
from datetime import timedelta
def write_html_table_title(fobj, parameters):
title = get_print_title_str(parameters)
fobj.write(f'<h3>{title}</h3>\n')
def write_html_text(fobj, text):
fobj.write(f'<p>{text}</p>\n')
def write_html_header(fobj, refresh_rate=10):
header = ['<!DOCTYPE html>',
'<html>',
'<head>',
' <link rel="stylesheet" media="only screen and (max-width: 400px)" href="mobile.css" />',
' <link rel="stylesheet" media="only screen and (min-width: 401px)" href="desktop.css" />',
'</head>',
f'<meta http-equiv="refresh" content="{refresh_rate}" >',
'<meta charset="utf-8">',
'<meta name="viewport" content="width=device-width, initial-scale=1">',
'<body>']
# style = ['<style>',
# 'table, th, td {',
# 'border:1px solid black;',
# '}',
# '</style>',]
for item in header:
fobj.write(item + '\n')
def init_html_table(fobj):
fobj.write('<table style="width:100%">\n')
def finish_html_table(fobj):
fobj.write('</table>\n')
def write_html_footer(fobj):
footer = ['</body>',
'</html>']
for item in footer:
fobj.write(item + '\n')
def write_html_row(fobj, items, html_key='td'):
default_space = ' '
fobj.write(default_space + '<tr>\n')
for item in items:
text = item.get('text')
if item.get('bold'):
text = '<b>' + text + '</b>'
if item.get('italic'):
text = '<i>' + text + '</i>'
tooltip = item.get('tooltip')
color = item.get('color')
# check for black background of headers (shouldnt happen anymore)
color = '#e6e6e6' if color == '#000000' else color
image_src = item.get('image_src')
image_str = f'<a href="{image_src}">' if image_src else ''
fobj.write(2 * default_space + f'<{html_key} bgcolor="{color}" title="{tooltip}"> {image_str}'
hyperlink = item.get('hyperlink')
image_str = f'<a href="{hyperlink}">' if hyperlink else ''
html_class = item.get('html_class')
class_str = f' class="{html_class}"' if html_class else ''
fobj.write(2 * default_space + f'<{html_key}{class_str} bgcolor="{color}" title="{tooltip}"> {image_str}'
+ text + f'</{html_key}>\n')
fobj.write(default_space + '</tr>\n')
def get_print_title_str(parameters):
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}'