diff --git a/utils/SnapPy/Snappy/EEMEP/Controller.py b/utils/SnapPy/Snappy/EEMEP/Controller.py index fb4acac1..a8071a1b 100644 --- a/utils/SnapPy/Snappy/EEMEP/Controller.py +++ b/utils/SnapPy/Snappy/EEMEP/Controller.py @@ -15,11 +15,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -''' +""" Created on Aug 9, 2016 @author: heikok -''' +""" from PyQt5 import QtWidgets from collections import deque @@ -34,18 +34,25 @@ from time import gmtime, strftime import traceback -from PyQt5.QtCore import QProcess, QProcessEnvironment, QThread, QIODevice, QThreadPool, pyqtSignal +from PyQt5.QtCore import ( + QProcess, + QProcessEnvironment, + QThread, + QIODevice, + QThreadPool, + pyqtSignal, +) from Snappy.BrowserWidget import BrowserWidget from Snappy.EEMEP.Resources import Resources from Snappy.EEMEP.ModelRunner import ModelRunner import Snappy.Utils + def getFileOwner(filename): pwuid = pwd.getpwuid(os.stat(filename).st_uid) return pwuid.pw_name, pwuid.pw_gecos - def debug(*objs): print("DEBUG: ", *objs, file=sys.stderr) @@ -71,18 +78,15 @@ def run(self): traceback.print_exc() - - -class Controller(): - ''' +class Controller: + """ Controller for EEMEP Widget. Starts the browserwidget as self.main and connects it to the form handler - ''' - + """ def __init__(self): - ''' + """ Initialize Widget and handlers - ''' + """ self.res = Resources() self.main = BrowserWidget() self.main.set_html(self.res.getStartScreen()) @@ -96,62 +100,75 @@ def __init__(self): self.lastLog = [] self.logfile_size = 0 - - def write_log(self, txt:str, max_lines=30, clear_log=False): - if (clear_log): + def write_log(self, txt: str, max_lines=30, clear_log=False): + if clear_log: self.lastLog = [txt] else: self.lastLog += txt.splitlines() debug(txt) - #Write at most 30 lines to screen - if (len(self.lastLog) > max_lines): + # Write at most 30 lines to screen + if len(self.lastLog) > max_lines: self.lastLog = self.lastLog[-max_lines:] lines = None - self.main.evaluate_javaScript('updateEemepLog({0});'.format(json.dumps("\n".join(self.lastLog)))) + self.main.evaluate_javaScript( + "updateEemepLog({0});".format(json.dumps("\n".join(self.lastLog))) + ) def update_log_query(self, qDict): - #MainBrowserWindow._default_form_handler(qDict) - #self.write_log("updating...") + # MainBrowserWindow._default_form_handler(qDict) + # self.write_log("updating...") if os.path.isfile(self.volcano_logfile): current_size = os.path.getsize(self.volcano_logfile) # Log overwritten - new file (this should not happen) - if (current_size < self.logfile_size): - self.write_log("WARNING: Logfile overwritten - someone else is running this volcano also") + if current_size < self.logfile_size: + self.write_log( + "WARNING: Logfile overwritten - someone else is running this volcano also" + ) self.logfile_size = 0 # If new content in logfile - if (current_size > self.logfile_size): + if current_size > self.logfile_size: with open(self.volcano_logfile) as lf: lf.seek(self.logfile_size) for line in lf: self.write_log(line) self.logfile_size = current_size else: - if (os.path.isfile(self.volcano_file)): - self.write_log("Queue busy {:%Y-%m-%d %H:%M:%S}".format(datetime.datetime.now())) - if (self.res.getModelRunnerLogs()): + if os.path.isfile(self.volcano_file): + self.write_log( + "Queue busy {:%Y-%m-%d %H:%M:%S}".format(datetime.datetime.now()) + ) + if self.res.getModelRunnerLogs(): self.write_log(self.res.getModelRunnerLogs()) else: - #Check if volcano logfile exists after waiting slightly + # Check if volcano logfile exists after waiting slightly # (to avoid race conditions in which volcano.xml is deleted before logfile is created) time.sleep(1.0) - if (os.path.isfile(self.volcano_logfile)): - self.write_log("ERROR: Neither '{:s}' \nnor '{:s}' Exists!\nSomeone may have deleted the run.".format(self.volcano_file, self.volcano_logfile)) + if os.path.isfile(self.volcano_logfile): + self.write_log( + "ERROR: Neither '{:s}' \nnor '{:s}' Exists!\nSomeone may have deleted the run.".format( + self.volcano_file, self.volcano_logfile + ) + ) self.eemepRunning = "inactive" def cancel_first_in_queue(self, qDict): - '''Mark all currently active model-runs for abort''' + """Mark all currently active model-runs for abort""" for dirpath, dirs, files in os.walk(self.res.getOutputDir()): for file in files: if file == ModelRunner.ABORT_FILENAME: try: self.write_log("trying to abort {}".format(dirpath)) - abortLogFile = datetime.datetime.now().strftime('{fname}_%Y%m%d-%H%M%S').format(fname=ModelRunner.ABORT_FILENAME) - with open(os.path.join(dirpath, abortLogFile), 'wt') as lh: + abortLogFile = ( + datetime.datetime.now() + .strftime("{fname}_%Y%m%d-%H%M%S") + .format(fname=ModelRunner.ABORT_FILENAME) + ) + with open(os.path.join(dirpath, abortLogFile), "wt") as lh: lh.write("aborted by {}".format(getpass.getuser())) os.remove(os.path.join(dirpath, file)) except: @@ -160,22 +177,36 @@ def cancel_first_in_queue(self, qDict): pass def cancel_submitted(self, qDict): - '''Cancel the last submitted volcano-file''' - if (os.path.isfile(self.volcano_file)): + """Cancel the last submitted volcano-file""" + if os.path.isfile(self.volcano_file): owner, gecos = getFileOwner(self.volcano_file) user = getpass.getuser() - debug("Deleting {:s} owned by {:s} ({:s}) with user {:s}".format(self.volcano_file, owner, gecos, user)) - if (owner != user): - self.write_log("WARNING: {:s}\nwas started by {:s} ({:s}). Please notify this user that you canceled the run!".format(self.volcano_file, owner, gecos)) + debug( + "Deleting {:s} owned by {:s} ({:s}) with user {:s}".format( + self.volcano_file, owner, gecos, user + ) + ) + if owner != user: + self.write_log( + "WARNING: {:s}\nwas started by {:s} ({:s}). Please notify this user that you canceled the run!".format( + self.volcano_file, owner, gecos + ) + ) try: os.remove(os.path.join(self.volcano_file)) self.write_log("{} deleted".format(self.volcano_file)) self.eemepRunning = "inactive" except Exception as e: - self.write_log("ERROR: could not cancel the currently submitted volcano!\n Error was {:s}".format(e.msg)) + self.write_log( + "ERROR: could not cancel the currently submitted volcano!\n Error was {:s}".format( + e.msg + ) + ) pass else: - self.write_log("Volcano file ('{:s}') does not exist".format(self.volcano_file)) + self.write_log( + "Volcano file ('{:s}') does not exist".format(self.volcano_file) + ) def update_log(self): self.update_log_query({}) @@ -183,10 +214,11 @@ def update_log(self): def _create_form_handler(self): def handler(queryDict): """a form-handler with closure for self""" - options = { 'Run' : self.run_eemep_query, - 'Update' : self.update_log_query, - 'Cancel+active': self.cancel_first_in_queue, - 'Cancel+submitted': self.cancel_submitted + options = { + "Run": self.run_eemep_query, + "Update": self.update_log_query, + "Cancel+active": self.cancel_first_in_queue, + "Cancel+submitted": self.cancel_submitted, } # mapping from QList to simple dictionary qDict = dict() @@ -194,14 +226,19 @@ def handler(queryDict): qDict[key] = value # calling the correct handler depending on the module try: - options[qDict['action']](qDict) + options[qDict["action"]](qDict) except TypeError as ex: self.write_log("type-error: {}".format(ex)) except ValueError as ex: self.write_log("value-error: {}".format(ex)) except: - self.write_log("Unexpected error on {0}: {1}".format(qDict['action'],sys.exc_info()[0])) + self.write_log( + "Unexpected error on {0}: {1}".format( + qDict["action"], sys.exc_info()[0] + ) + ) raise + return handler def run_eemep_query(self, qDict): @@ -212,64 +249,74 @@ def run_eemep_query(self, qDict): for key, value in qDict.items(): print(str.format("{0} => {1}", key, value)) errors = "" - match = re.search(r'(\d{4})-(\d{2})-(\d{2})[\+\s]+(\d{1,2})', qDict['startTime']) + match = re.search( + r"(\d{4})-(\d{2})-(\d{2})[\+\s]+(\d{1,2})", qDict["startTime"] + ) if match: - startTime = "{0} {1} {2} {3}".format(*match.group(1,2,3,4)) - startDT = datetime.datetime(*tuple(map(int, list(match.group(1,2,3,4))))) - modelStartDT = datetime.datetime(startDT.year, startDT.month, startDT.day, 0, 0, 0) + startTime = "{0} {1} {2} {3}".format(*match.group(1, 2, 3, 4)) + startDT = datetime.datetime(*tuple(map(int, list(match.group(1, 2, 3, 4))))) + modelStartDT = datetime.datetime( + startDT.year, startDT.month, startDT.day, 0, 0, 0 + ) else: - errors += "Cannot interpret startTime: {0}\n".format(qDict['startTime']) + errors += "Cannot interpret startTime: {0}\n".format(qDict["startTime"]) try: - runTime = int(qDict['runTime']) + runTime = int(qDict["runTime"]) except: - errors += "Cannot interpret runTime: {}\n".format(qDict['runTime']) + errors += "Cannot interpret runTime: {}\n".format(qDict["runTime"]) restart = "false" - if ('restart_file' in qDict and qDict['restart_file'].lower() == 'true'): - restart = 'restart' + if "restart_file" in qDict and qDict["restart_file"].lower() == "true": + restart = "restart" - if qDict['volcanotype'] == 'default': - type = 'M0' + if qDict["volcanotype"] == "default": + type = "M0" else: - type = qDict['volcanotype'] + type = qDict["volcanotype"] volcanoes = self.res.readVolcanoes() - if (qDict['volcano'] and volcanoes[qDict['volcano']]): - tag = qDict['volcano'] - volcano = re.sub(r'[^\w.-_]','_',volcanoes[qDict['volcano']]['NAME']) - latf = volcanoes[qDict['volcano']]['LATITUDE'] - lonf = volcanoes[qDict['volcano']]['LONGITUDE'] - altf = volcanoes[qDict['volcano']]['ELEV'] - if qDict['volcanotype'] == 'default': - type = volcanoes[qDict['volcano']]['ERUPTIONTYPE'] + if qDict["volcano"] and volcanoes[qDict["volcano"]]: + tag = qDict["volcano"] + volcano = re.sub(r"[^\w.-_]", "_", volcanoes[qDict["volcano"]]["NAME"]) + latf = volcanoes[qDict["volcano"]]["LATITUDE"] + lonf = volcanoes[qDict["volcano"]]["LONGITUDE"] + altf = volcanoes[qDict["volcano"]]["ELEV"] + if qDict["volcanotype"] == "default": + type = volcanoes[qDict["volcano"]]["ERUPTIONTYPE"] else: - lat = qDict['latitude'] - lon = qDict['longitude'] - alt = qDict['altitude'] + lat = qDict["latitude"] + lon = qDict["longitude"] + alt = qDict["altitude"] try: latf = Snappy.Utils.parseLat(lat) lonf = Snappy.Utils.parseLon(lon) altf = float(alt) except: - latf = 0. - lonf = 0. - altf = 0. - errors += "Cannot interpret latitude/longitude/altitude: {0}/{1}/{2}\n".format(lat,lon,alt); + latf = 0.0 + lonf = 0.0 + altf = 0.0 + errors += "Cannot interpret latitude/longitude/altitude: {0}/{1}/{2}\n".format( + lat, lon, alt + ) volcano = "{lat}N_{lon}E".format(lat=latf, lon=lonf) - debug("volcano: {0} {1:.2f} {2:.2f} {3} {4}".format(volcano, latf, lonf, altf, type)) + debug( + "volcano: {0} {1:.2f} {2:.2f} {3} {4}".format( + volcano, latf, lonf, altf, type + ) + ) try: volctype = self.res.readVolcanoType(type) except Exception as ex: errors += str(ex) + "\n" - errors += 'Please select Height and Type (Advanced) manually.\n' + errors += "Please select Height and Type (Advanced) manually.\n" # fine-ash fraction (particles < 63µm) try: - m63 = qDict['m63'] - if m63.lower() == 'mastin': - m63f = volctype['m63'] + m63 = qDict["m63"] + if m63.lower() == "mastin": + m63f = volctype["m63"] else: m63f = float(m63) if not (0 < m63f <= 1): @@ -277,71 +324,102 @@ def run_eemep_query(self, qDict): except Exception as ex: errors += str(ex) + "\n" - self.write_log("working with {:s} (lat={:.2f}N lon={:.2f}E) starting at {:s}".format(volcano, latf, lonf, startTime)) - + self.write_log( + "working with {:s} (lat={:.2f}N lon={:.2f}E) starting at {:s}".format( + volcano, latf, lonf, startTime + ) + ) # Get cloud height if supplied and calculate eruption rate - if qDict['cloudheight']: + if qDict["cloudheight"]: try: - cheight = float(qDict['cloudheight']) + cheight = float(qDict["cloudheight"]) except: - errors += "cannot interpret cloudheight (m): {0}\n".format(qDict['cloudheight']) + errors += "cannot interpret cloudheight (m): {0}\n".format( + qDict["cloudheight"] + ) - if (cheight % 1 != 0): - self.write_log("WARNING: Ash cloud height supplied with fraction. Please check that you supplied meters, not km!") + if cheight % 1 != 0: + self.write_log( + "WARNING: Ash cloud height supplied with fraction. Please check that you supplied meters, not km!" + ) - if qDict['cloudheight_datum'] == 'mean_sea_level': + if qDict["cloudheight_datum"] == "mean_sea_level": # Interpret cloud height as above sea level # - remove volcano vent altitude to get plume height - self.write_log("Ash cloud height measured from mean sea level: {:.2f} km".format(cheight/1000.0)) + self.write_log( + "Ash cloud height measured from mean sea level: {:.2f} km".format( + cheight / 1000.0 + ) + ) cheight = cheight - altf - elif qDict['cloudheight_datum'] == 'vent': + elif qDict["cloudheight_datum"] == "vent": # Interpret cloud height as above vent pass else: - errors += "cannot interpret cloud height datum: {:s}".format(qDict['cloudheight_datum']) + errors += "cannot interpret cloud height datum: {:s}".format( + qDict["cloudheight_datum"] + ) # rate in kg/s from Mastin et al. 2009, formular (1) and a volume (DRE) (m3) to # mass (kg) density of 2500kg/m3 - rate = 2500.0 * ((0.5*max(0, cheight)/1000.0)**(1.0/0.241)) + rate = 2500.0 * ((0.5 * max(0, cheight) / 1000.0) ** (1.0 / 0.241)) else: - cheight = float(volctype['H']) * 1000 # km -> m - rate = float(volctype['dM/dt']) - - #Check negative ash cloud height - if (cheight <= 0): - errors += "Negative cloud height {:.2f}! Please check ash cloud.".format(cheight/1000.0) - self.write_log("Ash cloud height measured from volcano: {:.2f} km, rate: {:.0f} kg/s, volcano height: {:.2f} km.".format(cheight/1000.0, rate, altf/1000.0)) + cheight = float(volctype["H"]) * 1000 # km -> m + rate = float(volctype["dM/dt"]) + + # Check negative ash cloud height + if cheight <= 0: + errors += "Negative cloud height {:.2f}! Please check ash cloud.".format( + cheight / 1000.0 + ) + self.write_log( + "Ash cloud height measured from volcano: {:.2f} km, rate: {:.0f} kg/s, volcano height: {:.2f} km.".format( + cheight / 1000.0, rate, altf / 1000.0 + ) + ) # Abort if errors - if (len(errors) > 0): - debug('updateLog("{0}");'.format(json.dumps("ERRORS:\n"+errors))) + if len(errors) > 0: + debug('updateLog("{0}");'.format(json.dumps("ERRORS:\n" + errors))) self.write_log("ERRORS:\n{0}".format(errors)) return # eEMEP runs up-to 23 km, so remove all ash above 23 km, # See Varsling av vulkanaske i norsk luftrom - driftsfase, # February 2020 for details - eemep_cheight_max = 23000.0-altf - if (cheight > eemep_cheight_max): + eemep_cheight_max = 23000.0 - altf + if cheight > eemep_cheight_max: rate_fraction = eemep_cheight_max / cheight - self.write_log("Cropping ash cloud to {:.2f} km from {:.2f} km using factor {:.3f}".format(eemep_cheight_max/1000.0, cheight/1000.0, rate_fraction)) + self.write_log( + "Cropping ash cloud to {:.2f} km from {:.2f} km using factor {:.3f}".format( + eemep_cheight_max / 1000.0, cheight / 1000.0, rate_fraction + ) + ) rate = rate * rate_fraction cheight = eemep_cheight_max eruptions = [] eruption = '' - eruptions.append(eruption.format(start=startDT.isoformat(), - end=(startDT + datetime.timedelta(hours=runTime)).isoformat(), - bottom=0, - top=cheight, - rate=rate, - m63=m63f)) - - self.lastOutputDir = os.path.join(self.res.getOutputDir(), "{0}_ondemand".format(volcano)) - self.volcano_file = os.path.join(self.lastOutputDir, ModelRunner.VOLCANO_FILENAME) + eruptions.append( + eruption.format( + start=startDT.isoformat(), + end=(startDT + datetime.timedelta(hours=runTime)).isoformat(), + bottom=0, + top=cheight, + rate=rate, + m63=m63f, + ) + ) + + self.lastOutputDir = os.path.join( + self.res.getOutputDir(), "{0}_ondemand".format(volcano) + ) + self.volcano_file = os.path.join( + self.lastOutputDir, ModelRunner.VOLCANO_FILENAME + ) self.lastQDict = qDict sourceTerm = """ @@ -357,34 +435,44 @@ def run_eemep_query(self, qDict): """ - ecModelRun = qDict['ecmodelrun']; + ecModelRun = qDict["ecmodelrun"] if not ecModelRun == "best": ecModelRun += "Z" - self.lastSourceTerm = sourceTerm.format(lat=latf, lon=lonf, - volcano=volcano, - alt=altf, - outdir=self.lastOutputDir, - restart=restart, - model_run=ecModelRun, - model_start_time=modelStartDT.isoformat(), - eruptions="\n".join(eruptions), - runTime=runTime) + self.lastSourceTerm = sourceTerm.format( + lat=latf, + lon=lonf, + volcano=volcano, + alt=altf, + outdir=self.lastOutputDir, + restart=restart, + model_run=ecModelRun, + model_start_time=modelStartDT.isoformat(), + eruptions="\n".join(eruptions), + runTime=runTime, + ) debug("output directory: {}".format(self.lastOutputDir)) os.makedirs(self.lastOutputDir, exist_ok=True) - self.volcano_logfile = os.path.join(self.lastOutputDir,"volcano.log") - if (os.path.exists(self.volcano_logfile)): - logdate = datetime.datetime.fromtimestamp(os.path.getmtime(self.volcano_logfile)) - os.rename(self.volcano_logfile, "{}_{}".format(self.volcano_logfile, logdate.strftime("%Y%m%dT%H%M%S"))) + self.volcano_logfile = os.path.join(self.lastOutputDir, "volcano.log") + if os.path.exists(self.volcano_logfile): + logdate = datetime.datetime.fromtimestamp( + os.path.getmtime(self.volcano_logfile) + ) + os.rename( + self.volcano_logfile, + "{}_{}".format(self.volcano_logfile, logdate.strftime("%Y%m%dT%H%M%S")), + ) try: # Mode x - open for exclusive creation, failing if the file already exists - with open(self.volcano_file,'x') as fh: + with open(self.volcano_file, "x") as fh: fh.write(self.lastSourceTerm) except FileExistsError as e: owner = "unknown" - if (os.path.exists(self.volcano_file)): + if os.path.exists(self.volcano_file): owner, gecos = getFileOwner(self.volcano_file) - errmsg = "ERROR: Run ({:s}) already exists!\nCreated by user {:s} ({:s}).\nPlease try again later.".format(self.volcano_file, owner, gecos) + errmsg = "ERROR: Run ({:s}) already exists!\nCreated by user {:s} ({:s}).\nPlease try again later.".format( + self.volcano_file, owner, gecos + ) debug('updateLog("{0}");'.format(json.dumps(errmsg))) self.write_log(errmsg) return @@ -395,6 +483,7 @@ def run_eemep_query(self, qDict): self.model_update.update_log_signal.connect(self.update_log) self.model_update.start(QThread.LowPriority) + if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) ctr = Controller()